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

Visualize the changes in the German gas price

If you live in Germany and want to make your own statistics about the changes in the gas price, or maybe want to integrate the gas price in your private Home Automation system display, you can do that by using the API from the website Tankerkönig.

You can get an API key and the full description of the API here. Once you have your personal API key, you can get the ID of your most favorite gas station by making an API call with the coordinates of your location and the radius to search within.

https://creativecommons.tankerkoenig.de/json/list.php?lat=52.521&lng=13.438&rad=1.5&sort=dist&type=all&apikey=YOURAPIKEY

You can just copy the call to the address bar of your browser and confirm with enter. Immediately, you should get a list of the gas stations that surround you within the given radius.

StationList

If you have found your favorite station within the list, you can use it’s unique ID to make an API call only for that particular station. I do this periodically, store the values in a RRDtool database and make graphs that show the development of the gas price during the last 24 hours and during the last seven days.

benzinpreis

benzinpreis7d

Below, you can see the Python script listed for making the API call and storing the result in a RRDtool database.

#!/usr/bin/env python

import sys
import os
import rrdtool
import urllib2
import json

try:
    # Get the gasprice for your desired gas station
    f = urllib2.urlopen('https://creativecommons.tankerkoenig.de/json/detail.php?id=DESIREDSTATIONID&apikey=YOURAPIKEY')
    json_string = f.read()
    parsed_json = json.loads(json_string)
    benzinpreis = parsed_json['station']['e5']
    f.close()
except:
    sys.exit(-1)

# Insert data into the database
data = "N:%.3f" % (benzinpreis)
rrdtool.update("%s/benzinpreis.rrd" % (os.path.dirname(os.path.abspath(__file__))), data)