Raspberry Pi Zero W + Pimoroni Scroll pHAT HD = IoT Display

For a long time already, I always wanted to have some kind of display that permanently shows me different information. – Information that is useful for me personally. Recently there are a lot of new display technologies, like OLED and electronic paper display, but also conventional multi line LCDs became cheaper and cheaper during the last years. But most of these displays are meant to be read from a short distance, and my idea of such an information display was different. I wanted it to be:

Readable from all over the apartment

Convenient to read in all light conditions

While reading the latest news about the Raspberry Pi, I some when found out that Pimoroni has released a LED display for the Raspberry Pi, called Scroll pHAT HD. That display looked like it was exactly what I was waiting for. Some may say that scrolling LED displays are a bit old fashioned, but in my opinion they are not. When I’m sitting at the dinning table in the morning and having breakfast, reading the display is fun and it reassures¬† me that the Scroll pHAT HD is just the right display for my purpose.

So, what to finally show on such a display? Actually, there’s not too much to think about this in my case, a small and compact summary of my weather station website, of course! ūüôā I came to the conclusion that the following information would be most useful and interesting for me:

Current weekday, date and time

Indoor temperature and indoor air quality

Outdoor weather data and conditions

Local gas price

Weekday, date and time are easy to get in Python, indoor temperature comes from my FRITZ!DECT 200, air quality from my air quality sensor via ThingSpeak, outdoor weather data and conditions from Weather Underground and the local gas price is provided by Tankerk√∂nig. Here we go…

Because of it’s compact size and the built in WI-FI, I connected the Scroll pHAT HD to a Raspberry Pi Zero W. In front of the display, I’ve placed a custom made diffusor foil for better readability. The foil was the front of a binder that I found in an office supply store, simple but very efficient. The Raspberry Pi Zero W has a small plastic case and sits on an original Raspberry Pi power supply, directly connected to a power outlet (you may also want to see the 3D printed case from this guy, which holds the Raspberry Pi Zero W and the Scroll pHAT HD within one enclosure). In the video below, you can see the finished result. The script that is used runs every ten minutes and displays the information six times in a row. Except weekday, date and time, all the other information is only gathered once¬†(at the beginning of the script) and it does not change during the ten minutes time period.

The script that I use is based on the advanced scrolling example from Pimoroni. This is already a good starting point for your own scripts. I especially like that you can put connected information into a single line, which then, after the horizontal scrolling of the line has finished, scrolls vertically to the next line of connected information.

But like usual, quite a bit of extra work on the script was required, until it finally worked in the way, it was supposed to. First of all, it is not possible to use mutated vowels (or other Unicode characters) within Python 2.7 without taking care of the encoding. There are a lot of discussions about that topic on the Internet and I ended up with doing something that actually should no be done, because I was just not able to find a better and cleaner solution that actually works. So, I’m aware that there still could be a bit room for improvement here.

Also, the font file that is originally provided by Pimoroni lacks of some characters that I want to display, that means that I had to modify it to show my own, custom characters/symbols, like the smileys, the superscript nine and the Euro sign. Actually this is not very hard to do. You just have to find an unused character in the font that you want to turn into a custom one and then edit the corresponding entry in the font file font5x7.py. If you are using Notepad++ and select ‘0xff’ somewhere, it will automatically highlight all other occurrences of ‘0xff’. This will help you to clearly see and modify the character.

Euro

For using the additional characters from the font file in the text you want to show on the display, you have to use unichr() in the code.

The major problem when adapting the script  for my purpose was actually, that the length of the text to display is not always the same. Since the weather conditions can be different, the total amount of the characters (responsively the total amount of the horizontal pixel) can vary. This means that the scrolling speed has to be a bit faster for longer texts and a bit slower for shorter texts, so that it can be displayed six times within the ten minutes time period, without having different waiting times at the end.

One should think that the scrolling speed and the total amount of the horizontal pixel are linearly dependent, but after a lot of trial and error, I have learned that this is not the case. Finally, I ended up with making some real world tests with different texts and deviating an equation that describes the connection between the total amount of he horizontal pixel and the necessary delay (responsively  the scrolling speed). All in all, it was a bit tricky, but I think the result is quite satisfying.

Graph

Below, you can see the entire script for the information display. It includes gathering all the information and displaying it in the way that is shown in the video above. The script also includes translation tables for the weather conditions and for the weekday that you can remove, if English is your first language. Of course, you can also use the tables to make your own translations into a different language than German.

Please note, that for reading the indoor temperature, the Bash script from the article ‘Reading the temperature from FRITZ!DECT devices’ is necessary. Additionally, YOURCHANNELID, YOURTHINGSPEAKAPIKEY, YOURAPIKEY, YOURFIELDNAME, YOURAPIKEYHERE, YOURSTATIONIDHERE, and DESIREDSTATIONID have to be replaced with your personal information.

#!/usr/bin/env python
# encoding: utf-8
import sys
import os
import shutil
import time
import urllib2
import json
import unicodedata
import scrollphathd
from scrollphathd.fonts import font5x7


reload(sys)
sys.setdefaultencoding("utf-8")


# Get the temperature from the FRITZ!DECT 200
try:
    process = os.popen('sudo ./fritztemp.sh')
    fritztemp = process.read()
    process.close()

    fritztemp = fritztemp.replace(',', '.')
    fritztemp = fritztemp.replace('\n', '')
except:
    fritztemp = '-'

indoortemp = 'Innentemperatur: ' + fritztemp + ' ' + unichr(20) + 'C'


# Get the air quality from ThingSpeak
try:
    f = urllib2.urlopen('http://api.thingspeak.com/channels/YOURCHANNELID/feeds/last.json?api_key=YOURTHINGSPEAKAPIKEY')
    json_string = f.read()
    parsed_json = json.loads(json_string)
    ppm = parsed_json['YOURFIELDNAME']
    f.close()

    ppm_float = float(ppm)

    smiley = ''
    if (ppm_float < 1000):
       smiley = ' ' + unichr(40) + unichr(2) + unichr(41)
    elif (ppm_float >= 1000 and ppm_float < 1500):
       smiley = ' ' + unichr(40) + unichr(3) + unichr(41)
    elif (ppm_float >= 1500):
       smiley = ' ' + unichr(40) + unichr(4) + unichr(41)
except:
    ppm = '-'
    smiley = ''

airquality = 'Luftqualit' + unichr(145) + 't: ' + ppm + ' ppm' + smiley


# Get the weather data from Weather Underground
try:
    f = urllib2.urlopen('http://api.wunderground.com/api/YOURAPIKEYHERE/geolookup/conditions/q/pws:YOURSTATIONIDHERE.json')
    json_string = f.read()
    parsed_json = json.loads(json_string)
    temp_c = parsed_json['current_observation']['temp_c']
    relative_humidity = parsed_json['current_observation']['relative_humidity']
    relative_humidity = relative_humidity.replace('%', '')
    pressure_in = parsed_json['current_observation']['pressure_in']
    pressure_trend = parsed_json['current_observation']['pressure_trend']
    conditions = parsed_json['current_observation']['weather']
    f.close()

    temp_str = '%.1f' % temp_c
    press_float = float(pressure_in)
    press_float = press_float*33.8637526
    press_str =  '%.1f' % press_float

    trend = ''
    if pressure_trend == '0':
      trend = ' ' + unichr(28)
    elif pressure_trend == '+':
      trend = ' ' + unichr(30)
    elif pressure_trend == '-':
      trend = ' ' + unichr(31)

    conditions = conditions.replace('Light ', '')
    conditions = conditions.replace('Heavy ', '')

    # Make a German translation
    if conditions == 'Drizzle':
      conditions = conditions.replace('Drizzle', 'Nieselregen')
    elif conditions == 'Rain':
      conditions = conditions.replace('Rain', 'Regen')
    elif conditions == 'Snow':
      conditions = conditions.replace('Snow', 'Schnee')
    elif conditions == 'Snow Grains':
      conditions = conditions.replace('Snow Grains', 'Griesel')
    elif conditions == 'Ice Crystals':
      conditions = conditions.replace('Ice Crystals', 'Eisgl' + unichr(145) + 'tte')
    elif conditions == 'Ice Pellets':
      conditions = conditions.replace('Ice Pellets', 'Graupel')
    elif conditions == 'Hail':
      conditions = conditions.replace('Hail', 'Hagel')
    elif conditions == 'Mist':
      conditions = conditions.replace('Mist', 'Feuchter Dunst')
    elif conditions == 'Fog':
      conditions = conditions.replace('Fog', 'Nebel')
    elif conditions == 'Fog Patches':
      conditions = conditions.replace('Fog Patches', 'Nebelschwaden')
    elif conditions == 'Smoke':
      conditions = conditions.replace('Smoke', 'Rauch')
    elif conditions == 'Volcanic Ash':
      conditions = conditions.replace('Volcanic Ash', 'Vulkanasche')
    elif conditions == 'Widespread Dust':
      conditions = conditions.replace('Widespread Dust', 'Verbreitet staubig')
    elif conditions == 'Sand':
      conditions = conditions.replace('Sand', 'Sandig')
    elif conditions == 'Haze':
      conditions = conditions.replace('Haze', 'Dunst')
    elif conditions == 'Spray':
      conditions = conditions.replace('Spray', 'Spr' + unichr(177) + 'hregen')
    elif conditions == 'Dust Whirls':
      conditions = conditions.replace('Dust Whirls', 'Wirbelwind')
    elif conditions == 'Sandstorm':
      conditions = conditions.replace('Sandstorm', 'Sandsturm')
    elif conditions == 'Low Drifting Snow':
      conditions = conditions.replace('Low Drifting Snow', 'Schneeverwehungen')
    elif conditions == 'Low Drifting Widespread Dust':
      conditions = conditions.replace('Low Drifting Widespread Dust', 'Staubverwehungen')
    elif conditions == 'Low Drifting Sand':
      conditions = conditions.replace('Low Drifting Sand', 'Sandverwehungen')
    elif conditions == 'Blowing Snow':
      conditions = conditions.replace('Blowing Snow', 'Schneetreiben')
    elif conditions == 'Blowing Widespread Dust':
      conditions = conditions.replace('Blowing Widespread Dust', 'Staubtreiben')
    elif conditions == 'Blowing Sand':
      conditions = conditions.replace('Blowing Sand', 'Sandtreiben')
    elif conditions == 'Rain Mist':
      conditions = conditions.replace('Rain Mist', 'Spr' + unichr(177) + 'hregen')
    elif conditions == 'Rain Showers':
      conditions = conditions.replace('Rain Showers', 'Regenschauer')
    elif conditions == 'Snow Showers':
      conditions = conditions.replace('Snow Showers', 'Schneeschauer')
    elif conditions == 'Snow Blowing Snow Mist':
      conditions = conditions.replace('Snow Blowing Snow Mist', 'Schneegest' + unichr(169) + 'ber')
    elif conditions == 'Ice Pellet Showers':
      conditions = conditions.replace('Ice Pellet Showers', 'Graupelschauer')
    elif conditions == 'Hail Showers':
      conditions = conditions.replace('Hail Showers', 'Hagelschauer')
    elif conditions == 'Small Hail Showers':
      conditions = conditions.replace('Small Hail Showers', 'Kleink' + unichr(169) + 'rniger Hagelschauer')
    elif conditions == 'Thunderstorm':
      conditions = conditions.replace('Thunderstorm', 'Gewitter')
    elif conditions == 'Thunderstorms and Rain':
      conditions = conditions.replace('Thunderstorms and Rain', 'Gewitter und Regen')
    elif conditions == 'Thunderstorms and Snow':
      conditions = conditions.replace('Thunderstorms and Snow', 'Gewitter und Schnee')
    elif conditions == 'Thunderstorms and Ice Pellets':
      conditions = conditions.replace('Thunderstorms and Ice Pellets', 'Gewitter und Graupel')
    elif conditions == 'Thunderstorms with Hail':
      conditions = conditions.replace('Thunderstorms with Hail', 'Gewitter mit Hagel')
    elif conditions == 'Thunderstorms with Small Hail':
      conditions = conditions.replace('Thunderstorms with Small Hail', 'Gewitter mit kleink' + unichr(169) + 'rnigem Hagel')
    elif conditions == 'Freezing Drizzle':
      conditions = conditions.replace('Freezing Drizzle', 'Ueberfrierender Nieselregen')
    elif conditions == 'Freezing Rain':
      conditions = conditions.replace('Freezing Rain', 'Ueberfrierender Regen')
    elif conditions == 'Freezing Fog':
      conditions = conditions.replace('Freezing Fog', 'Ueberfrierender Nebel')
    elif conditions == 'Patches of Fog':
      conditions = conditions.replace('Patches of Fog', 'Nebelfelder')
    elif conditions == 'Shallow Fog':
      conditions = conditions.replace('Shallow Fog', 'Bodennebel')
    elif conditions == 'Partial Fog':
      conditions = conditions.replace('Partial Fog ', 'Teilweise neblig')
    elif conditions == 'Overcast':
      conditions = conditions.replace('Overcast', 'Bedeckt')
    elif conditions == 'Clear':
      conditions = conditions.replace('Clear', 'Klar')
    elif conditions == 'Partly Cloudy':
      conditions = conditions.replace('Partly Cloudy', 'Teilweise bew' + unichr(169) + 'lkt')
    elif conditions == 'Mostly Cloudy':
      conditions = conditions.replace('Mostly Cloudy', 'Meist bew' + unichr(169) + 'lkt')
    elif conditions == 'Cloudy':
      conditions = conditions.replace('Cloudy', 'Bew' + unichr(169) + 'lkt')
    elif conditions == 'Scattered Clouds':
      conditions = conditions.replace('Scattered Clouds', 'Aufgelockerte Bew' + unichr(169) + 'lkung')
    elif conditions == 'Small Hail':
      conditions = conditions.replace('Small Hail', 'Graupel')
    elif conditions == 'Squalls':
      conditions = conditions.replace('Squalls', 'Sturmb' + unichr(169) + 'en')
    elif conditions == 'Funnel Cloud':
      conditions = conditions.replace('Funnel Cloud', 'Trichterwolke')
    elif conditions == 'Unknown Precipitation':
      conditions = conditions.replace('Unknown Precipitation', 'Unbekannter Niederschlag')
    elif conditions == 'Unknown':
      conditions = conditions.replace('Unknown', 'Unbekannt')
except:
    temp_str = '-'
    relative_humidity = '-'
    press_str = '-'
    trend = ''
    conditions = '-'

weather = 'Aussentemperatur: ' + temp_str + ' ' + unichr(20) + 'C -' + ' Aussenluftfeuchtigkeit: ' + relative_humidity + ' % -' + ' Relativer Luftdruck: ' + press_str + ' hPa' + trend + ' -' + ' Wetterbedingungen: '  + conditions


# Get the gas price from Tankerkönig
try:
    f = urllib2.urlopen('https://creativecommons.tankerkoenig.de/json/detail.php?id=DESIREDSTATIONID&apikey=YOURAPIKEY')
    json_string = f.read()
    parsed_json = json.loads(json_string)
    gas = parsed_json['station']['e5']
    f.close()

    gas_str = '%.2f' % gas
except:
    gas_str = '-'

if gas_str == '-':
    gasprice = 'Benzinpreis Super E5: ' + gas_str + ' ' + unichr(91)
else:
    gasprice = 'Benzinpreis Super E5: ' + gas_str + unichr(36) + ' ' + unichr(91)


space = '    '
hyphen = ' - '
indoor = indoortemp + hyphen + airquality + space
weather = weather + space
gasprice = gasprice + space


# Show the thext on the display
for x in range(0, 6):
    try:
        # Get the weekday, date and time
        datetime = time.strftime('%A, %d.%m.%Y - %H:%M Uhr')
        # Make a German translation
        datetime = datetime.replace('Monday', 'Montag')
        datetime = datetime.replace('Tuesday', 'Dienstag')
        datetime = datetime.replace('Wednesday', 'Mittwoch')
        datetime = datetime.replace('Thursday', 'Donnerstag')
        datetime = datetime.replace('Friday', 'Freitag')
        datetime = datetime.replace('Saturday', 'Samstag')
        datetime = datetime.replace('Sunday', 'Sonntag')

        datetime = datetime + space

        scrollphathd.clear()
        scrollphathd.set_brightness(0.2)

        lines = [datetime,
                 indoor,
                 weather,
                 gasprice]

        line_height = scrollphathd.DISPLAY_HEIGHT + 2
        offset_left = 0

        lengths = [0] * len(lines)
        for line, text in enumerate(lines):
            lengths[line] = scrollphathd.write_string(text, x=offset_left, y=line_height * line, font=euro5x7)
            offset_left += lengths[line]

        scrollphathd.set_pixel(offset_left - 1, (len(lines) * line_height) - 1, 0)

        period = 96
        length = lengths[0] + lengths[1] + lengths[2] + lengths[3]
        # Factor bigger  -> Delay shorter -> Text runs faster
        # Factor smaller -> Delay longer  -> Text runs slower
        delay = period/(length*float(2.8))
        # The relationship between the amount of horizontal pixels and the scroll time is not linearly dependent 
        # Therefore, do a correction based on meassurements to adapt it to the conditions observed in reality
        delay = ((-0.0013889*length)+2.83472)*delay

        scrollphathd.scroll_to(0, 0)
        scrollphathd.show()

        # Scroll the text
        for current_line, line_length in enumerate(lengths):
            # Delay a slightly longer time at the start of each line
            time.sleep(delay*10)

            # Scroll to the end of the current line
            for x in range(line_length):
                scrollphathd.scroll(1, 0)
                scrollphathd.show()
                time.sleep(delay)

            if current_line != len(lines) - 1:
                # Progress to the next line by scrolling upwards
                for y in range(line_height):
                    scrollphathd.scroll(0, 1)
                    scrollphathd.show()
                    time.sleep(delay)
    except:
        scrollphathd.clear()
        sys.exit(-1)


scrollphathd.clear()

Overview of my weather station website

The website of my weather station includes contents from most of the articles here in the blog, so I finally want to give you an overview, of how the website that I mentioned several times before looks like.

Because I included all the information on one single page, I put a navigation bar at the top of the page. If you click on one of the links, the page scrolls automatically to the correspondent area. By clicking on one of the graphs, you will get back to the top of the page, so that there is actually no excessively scrolling necessary, despite the fact that the page is actually rather long. If you stay on the website for a longer period of time, it will automatically reload itself after five minutes, so that it always shows you the latest values.

website

Determining the indoor air quality with your Raspberry Pi

If you are considering to measure the air quality inside your house or apartment and start reading some online resources about that topic, for sure you will find a lot of information about the famous MQ-sensors. Especially the MQ135, which is designed to determine the air quality, would be a perfect sensor for that task. But the big downside of these sensors is that they deliver an analog signal, so that an additional A/D converter or a micro controller, like e.g. the Arduino, is necessary to convert the analog signal to a digital signal that then can be used by the Raspberry Pi. For sure, this is not such a big deal, but I was looking for a more ‘consolidated’ solution.

Finally, I found an air quality sensor, manufactured by Rehau, that can be connected directly to an USB port, already containing the sensor and a micro controller in the case of a small USB stick. This sensor does not just simply measures the amount of carbon dioxide and takes it as an indicator for the air quality, like e.g. the Netatmo weather station does, it is sensing volatile organic compounds that are produced by cooking, breathing, sweating and so on. Together with the amount of carbon monoxide, the sensor calculates a value for the air quality in ppm. Values up to 1000ppm are standing for good air quality, values from 1000ppm to 1500ppm representing mediocre air quality, and if the value exceeds 1500ppm, the air quality is considered bad. You can also see the current condition of the air directly at the stick, since it is shown by using a green, a yellow and a red LED.

Important thing about this stick is, that you have to do a calibration before you are using it for measuring and recording the air quality. For that, you just have to plug the stick in, when it is surrounded by clean and fresh air, that’s basically all. Unfortunately, the stick does not keep the value from the calibration permanently. That means, every time it is powered up (or the Raspberry Pi is restarted), the calibration happens again. But there is a little trick, with which you can avoid a new calibration after a restart. The sensor comes together with a Windows software called ‘Airmonitor’. If you start this software, press and hold ‘Strg’ and then click on the logo three times, the so called ‘Expert Mode’ within the software will be activated. After that, you can go to ‘Support Tools’, ‘Edit Knobs’ and change the the value for ‘ui16StartupBits’ to ‘0’. For further reading about the Rehau Air Quality Sensor, I recommend the article here.

Fortunately,  a program for Linux already exists that can be used on the Raspberry Pi to read the values from the sensor. You can easily download the sources of the program an build an executable, by simply following the steps provided by the author of the program.

sudo apt-get install libusb-dev
sudo apt-get install build-essential
mkdir airsensor
cd airsensor
wget http://usb-sensors-linux.googlecode.com/svn/trunk/airsensor/airsensor.c
gcc -o airsensor airsensor.c -lusb

Like all the other data from my weather station, and also the system information from the Raspberry Pi, I also store the air quality values in a RRDtool database and use it for making a graph that shows the air quality values from the last 24 hours. In that graph, I also include the thresholds for good, mediocre and bad air quality, so that it is very easy to notice, in which condition the air was at a given point in time.

airquality

Additionally, I included a little smiley icon in the status bar of my weather station website, to indicate the current condition of the air in the same way, I did for showing the warning level of my lightning detector.

Happy     Air quality is good

Neutral     Air quality is mediocre

Sad     Air quality is bad

This is what the smiley indicator looks like in the status bar of the website of my weather station, together with the weather forecast, the current season, sunset and sunrise times and the current moon phase.

Benningen

I also upload the values for the air quality to ThingSpeak and use the IoT ThingSpeak Monitor Widget to monitor the air quality on my Smartphone. This little widget can also generate a notification, if the air quality exceeds a specified threshold, so that you are reminded to open the windows and get some fresh air into the room, if you want.

Telefon

This is the Python script that I use for reading the air quality, updating the icon that represents the current condition of the air, and uploading the value to ThingSpeak. If you also have an account at ThingSpeak, you can replace YOURTHINGSPEAKWRITEKEY with your own personal write key. Please note that reading the value from the sensor can take several seconds, so if you e.g. call the Python script from within a Bash script, it should be followed by a sleep of about 20 seconds.

#!/usr/bin/python

import sys
import os
import shutil
import rrdtool
import httplib, urllib
import time

writekey = 'YOURTHINGSPEAKWRITEKEY'

# Read the current airquality
process = os.popen('sudo ./airsensor -o')
str = process.read()
process.close()

if not str:
	sys.exit(1)

str = str.strip()
str = str.replace(',', '')
data = str.split(' ')

ppm = int(float(data[3]))

# Check the output and store it in the database, if it is ok
if ppm > 0:
	# Insert data into database
	update = 'N:' + data[3]
	rrdtool.update("%s/airquality.rrd" % (os.path.dirname(os.path.abspath(__file__))), update)
	if (ppm < 1000):
		shutil.copyfile('/home/pi/airsensor/Happy.png', '/var/www/html/Smiley.png')
	elif (ppm >= 1000 and ppm < 1500):
		shutil.copyfile('/home/pi/airsensor/Neutral.png', '/var/www/html/Smiley.png')
	elif (ppm >= 1500):
		shutil.copyfile('/home/pi/airsensor/Sad.png', '/var/www/html/Smiley.png')
	# Post to ThingSpeak
	try:
		params = urllib.urlencode({'field1': ppm, 'key':writekey })
		headers = {"Content-typZZe": "application/x-www-form-urlencoded","Accept": "text/plain"}
		conn = httplib.HTTPConnection("api.thingspeak.com:80")
		conn.request("POST", "/update", params, headers)
		response = conn.getresponse()
		data = response.read()
		conn.close()
	except:
		sys.exit(-1)