Raspberry Pi PiTFT Weather Station

IMG_5218More tinkering with the wonderful Adafruit 2.8″ Touchscreen TFT module (PiTFT) for the Raspberry Pi.  This time a weather station drawing data from weather.com.

Luckily there’s a wonderful python module to extract data from three popular weather services; python-weather-api supports NOAA, Yahoo! Weather and weather.com.  This makes life so much easier.

Download the module and install in the usual way; there are instructions in their wiki.

A simple way to display the raw data in a more readable form is to use Pretty Print (pprint) which is installed by default on Respbian.  Just change the code in the call to weather.com in the script below to your town which can be found in the URL if you use the weather.com web page.

import pywapi
import pprint
pp = pprint.PrettyPrinter(indent=4)

kalamata = pywapi.get_weather_from_weather_com('UKXX1087')

pp.pprint(kalamata)

This returns the data from weather.com in a much more easy to digest form.  I used this to pull out the fields I wanted for my weather station.  The following is the output for the town of Lincoln UK (where I live):

{   'current_conditions': {   'barometer': {   'direction': u'steady',
                                               'reading': u'1026.08'},
                              'dewpoint': u'13',
                              'feels_like': u'17',
                              'humidity': u'80',
                              'icon': u'30',
                              'last_updated': u'6/14/14 11:25 AM BST',
                              'moon_phase': {   'icon': u'16',
                                                'text': u'Waning Gibbous'},
                              'station': u'Waddington, LIN, UK',
                              'temperature': u'17',
                              'text': u'Partly Cloudy',
                              'uv': {   'index': u'4', 'text': u'Moderate'},
                              'visibility': u'12.9',
                              'wind': {   'direction': u'50',
                                          'gust': u'N/A',
                                          'speed': u'12',
                                          'text': u'NE'}},
    'forecasts': [   {   'date': u'Jun 14',
                         'day': {   'brief_text': u'P Cloudy',
                                    'chance_precip': u'20',
                                    'humidity': u'72',
                                    'icon': u'30',
                                    'text': u'Partly Cloudy',
                                    'wind': {   'direction': u'32',
                                                'gust': u'N/A',
                                                'speed': u'19',
                                                'text': u'NNE'}},
                         'day_of_week': u'Saturday',
                         'high': u'18',
                         'low': u'10',
                         'night': {   'brief_text': u'M Clear',
                                      'chance_precip': u'10',
                                      'humidity': u'87',
                                      'icon': u'33',
                                      'text': u'Mostly Clear',
                                      'wind': {   'direction': u'16',
                                                  'gust': u'N/A',
                                                  'speed': u'17',
                                                  'text': u'NNE'}},
                         'sunrise': u'4:34 AM',
                         'sunset': u'9:31 PM'},
                     {   'date': u'Jun 15',
                         'day': {   'brief_text': u'P Cloudy',
                                    'chance_precip': u'10',
                                    'humidity': u'70',
                                    'icon': u'30',
                                    'text': u'Partly Cloudy',
                                    'wind': {   'direction': u'19',
                                                'gust': u'N/A',
                                                'speed': u'19',
                                                'text': u'NNE'}},
                         'day_of_week': u'Sunday',
                         'high': u'18',
                         'low': u'10',
                         'night': {   'brief_text': u'P Cloudy',
                                      'chance_precip': u'10',
                                      'humidity': u'87',
                                      'icon': u'29',
                                      'text': u'Partly Cloudy',
                                      'wind': {   'direction': u'23',
                                                  'gust': u'N/A',
                                                  'speed': u'16',
                                                  'text': u'NNE'}},
                         'sunrise': u'4:34 AM',
                         'sunset': u'9:31 PM'},
                     {   'date': u'Jun 16',
                         'day': {   'brief_text': u'Cloudy',
                                    'chance_precip': u'0',
                                    'humidity': u'76',
                                    'icon': u'26',
                                    'text': u'Cloudy',
                                    'wind': {   'direction': u'4',
                                                'gust': u'N/A',
                                                'speed': u'20',
                                                'text': u'N'}},
                         'day_of_week': u'Monday',
                         'high': u'15',
                         'low': u'10',
                         'night': {   'brief_text': u'Clear Late',
                                      'chance_precip': u'0',
                                      'humidity': u'78',
                                      'icon': u'29',
                                      'text': u'Clouds Early / Clearing Late',
                                      'wind': {   'direction': u'1',
                                                  'gust': u'N/A',
                                                  'speed': u'17',
                                                  'text': u'N'}},
                         'sunrise': u'4:34 AM',
                         'sunset': u'9:31 PM'},
                     {   'date': u'Jun 17',
                         'day': {   'brief_text': u'P Cloudy',
                                    'chance_precip': u'0',
                                    'humidity': u'75',
                                    'icon': u'30',
                                    'text': u'Partly Cloudy',
                                    'wind': {   'direction': u'13',
                                                'gust': u'N/A',
                                                'speed': u'20',
                                                'text': u'NNE'}},
                         'day_of_week': u'Tuesday',
                         'high': u'17',
                         'low': u'11',
                         'night': {   'brief_text': u'M Cloudy',
                                      'chance_precip': u'10',
                                      'humidity': u'82',
                                      'icon': u'27',
                                      'text': u'Mostly Cloudy',
                                      'wind': {   'direction': u'1',
                                                  'gust': u'N/A',
                                                  'speed': u'16',
                                                  'text': u'N'}},
                         'sunrise': u'4:34 AM',
                         'sunset': u'9:31 PM'},
                     {   'date': u'Jun 18',
                         'day': {   'brief_text': u'M Cloudy',
                                    'chance_precip': u'10',
                                    'humidity': u'75',
                                    'icon': u'28',
                                    'text': u'Mostly Cloudy',
                                    'wind': {   'direction': u'12',
                                                'gust': u'N/A',
                                                'speed': u'19',
                                                'text': u'NNE'}},
                         'day_of_week': u'Wednesday',
                         'high': u'19',
                         'low': u'10',
                         'night': {   'brief_text': u'P Cloudy',
                                      'chance_precip': u'0',
                                      'humidity': u'82',
                                      'icon': u'29',
                                      'text': u'Partly Cloudy',
                                      'wind': {   'direction': u'10',
                                                  'gust': u'N/A',
                                                  'speed': u'9',
                                                  'text': u'N'}},
                         'sunrise': u'4:34 AM',
                         'sunset': u'9:31 PM'}],
    'location': {   'lat': u'53.23',
                    'lon': u'-0.54',
                    'name': u'Lincoln, LIN, United Kingdom'},
    'units': {   'distance': u'km',
                 'pressure': u'mb',
                 'rainfall': u'mm',
                 'speed': u'km/h',
                 'temperature': u'C'}}

Another thing that made this project so much simpler was access to some great weather icons on the internet.  I have no artistic skill or patience for such in me.  I found some great icons produced by MerlinTheRed and available on Deviant Art.  These are free for non-commercial use.  I’ve included the copyright notice on GitHub.  The great thing about these, apart from looking good, is that the filename numbers match the icon numbers in the weather.com data!  Makes scripting the right icon so simple.

Installation & setup instructions:

  1. Install & configure your Raspberry Pi and the PiTFT using Adafruit’s fantastic instructions.
  2. Download & install the python-weather-api python module.
  3. Download & install my python daemon module.
  4. Create a directory for your weather station code.  I use /opt/PiTFTWeather/
  5. Download the script and icon images from my GitHub repository.

Start the script as root (sudo if you like):

# python PiTFTWeather.py start

Stop the script:

# python PiTFTWeather.py start

You should see a screen similar to this:

PiTFT Weather Station

PiTFT Weather Station

The following is the script itself:

import os, syslog
import pygame
import time
import pywapi
import string

from daemon import *

# Weather Icons used with the following permissions:
#
# VClouds Weather Icons©
# Created and copyrighted© by VClouds - http://vclouds.deviantart.com/
#
# The icons are free to use for Non-Commercial use, but If you use want to use it with your art please credit me and put a link leading back to the icons DA page - http://vclouds.deviantart.com/gallery/#/d2ynulp
#
# *** Not to be used for commercial use without permission!
# if you want to buy the icons for commercial use please send me a note - http://vclouds.deviantart.com/ ***

installPath = "/opt/PiTFTWeather/"

# location for Lincoln, UK on weather.com
weatherDotComLocationCode = 'UKXX1087'
# convert mph = kpd / kphToMph
kphToMph = 1.60934400061

# font colours
colourWhite = (255, 255, 255)
colourBlack = (0, 0, 0)

# update interval
updateRate = 600 # seconds

class pitft :
    screen = None;
    colourBlack = (0, 0, 0)

    def __init__(self):
        "Ininitializes a new pygame screen using the framebuffer"
        # Based on "Python GUI in Linux frame buffer"
        # http://www.karoltomala.com/blog/?p=679
        disp_no = os.getenv("DISPLAY")
        if disp_no:
            print "I'm running under X display = {0}".format(disp_no)

        os.putenv('SDL_FBDEV', '/dev/fb1')

        # Select frame buffer driver
        # Make sure that SDL_VIDEODRIVER is set
        driver = 'fbcon'
        if not os.getenv('SDL_VIDEODRIVER'):
            os.putenv('SDL_VIDEODRIVER', driver)
        try:
            pygame.display.init()
        except pygame.error:
            print 'Driver: {0} failed.'.format(driver)
            exit(0)

        size = (pygame.display.Info().current_w, pygame.display.Info().current_h)
        self.screen = pygame.display.set_mode(size, pygame.FULLSCREEN)
        # Clear the screen to start
        self.screen.fill((0, 0, 0))
        # Initialise font support
        pygame.font.init()
        # Render the screen
        pygame.display.update()

    def __del__(self):
        "Destructor to make sure pygame shuts down, etc."

# Create an instance of the PyScope class
mytft = pitft()

pygame.mouse.set_visible(False)

# set up the fonts
# choose the font
fontpath = pygame.font.match_font('dejavusansmono')
# set up 2 sizes
font = pygame.font.Font(fontpath, 20)
fontSm = pygame.font.Font(fontpath, 18)

# Inherit from Daemon class
class MyDaemon(daemon):
    # implement run method
    def run(self):
            while True:
                # retrieve data from weather.com
                weather_com_result = pywapi.get_weather_from_weather_com(weatherDotComLocationCode)

                # extract current data for today
                today = weather_com_result['forecasts'][0]['day_of_week'][0:3] + " " \
                    + weather_com_result['forecasts'][0]['date'][4:] + " " \
                    + weather_com_result['forecasts'][0]['date'][:3]
                windSpeed = int(weather_com_result['current_conditions']['wind']['speed']) / kphToMph
                currWind = "{:.0f}mph ".format(windSpeed) + weather_com_result['current_conditions']['wind']['text']
                currTemp = weather_com_result['current_conditions']['temperature'] + u'\N{DEGREE SIGN}' + "C"
                currPress = weather_com_result['current_conditions']['barometer']['reading'][:-3] + "mb"
                uv = "UV {}".format(weather_com_result['current_conditions']['uv']['text'])
                humid = "Hum {}%".format(weather_com_result['current_conditions']['humidity'])

                # extract forecast data
                forecastDays = {}
                forecaseHighs = {}
                forecaseLows = {}
                forecastPrecips = {}
                forecastWinds = {}

                start = 0
                try:
                    test = float(weather_com_result['forecasts'][0]['day']['wind']['speed'])
                except ValueError:
                    start = 1

                for i in range(start, 5):

                    if not(weather_com_result['forecasts'][i]):
                        break
                    forecastDays[i] = weather_com_result['forecasts'][i]['day_of_week'][0:3]
                    forecaseHighs[i] = weather_com_result['forecasts'][i]['high'] + u'\N{DEGREE SIGN}' + "C"
                    forecaseLows[i] = weather_com_result['forecasts'][i]['low'] + u'\N{DEGREE SIGN}' + "C"
                    forecastPrecips[i] = weather_com_result['forecasts'][i]['day']['chance_precip'] + "%"
                    forecastWinds[i] = "{:.0f}".format(int(weather_com_result['forecasts'][i]['day']['wind']['speed'])  / kphToMph) + \
                        weather_com_result['forecasts'][i]['day']['wind']['text']

                # blank the screen
                mytft.screen.fill(colourBlack)

                # Render the weather logo at 0,0
                icon = installPath+ (weather_com_result['current_conditions']['icon']) + ".png"
                logo = pygame.image.load(icon).convert()
                mytft.screen.blit(logo, (0, 0))

                # set the anchor for the current weather data text
                textAnchorX = 140
                textAnchorY = 5
                textYoffset = 20

                # add current weather data text artifacts to the screen
                text_surface = font.render(today, True, colourWhite)
                mytft.screen.blit(text_surface, (textAnchorX, textAnchorY))
                textAnchorY+=textYoffset
                text_surface = font.render(currTemp, True, colourWhite)
                mytft.screen.blit(text_surface, (textAnchorX, textAnchorY))
                textAnchorY+=textYoffset
                text_surface = font.render(currWind, True, colourWhite)
                mytft.screen.blit(text_surface, (textAnchorX, textAnchorY))
                textAnchorY+=textYoffset
                text_surface = font.render(currPress, True, colourWhite)
                mytft.screen.blit(text_surface, (textAnchorX, textAnchorY))
                textAnchorY+=textYoffset
                text_surface = font.render(uv, True, colourWhite)
                mytft.screen.blit(text_surface, (textAnchorX, textAnchorY))
                textAnchorY+=textYoffset
                text_surface = font.render(humid, True, colourWhite)
                mytft.screen.blit(text_surface, (textAnchorX, textAnchorY))

                # set X axis text anchor for the forecast text
                textAnchorX = 0
                textXoffset = 65

                # add each days forecast text
                for i in forecastDays:
                    textAnchorY = 130
                    text_surface = fontSm.render(forecastDays[int(i)], True, colourWhite)
                    mytft.screen.blit(text_surface, (textAnchorX, textAnchorY))
                    textAnchorY+=textYoffset
                    text_surface = fontSm.render(forecaseHighs[int(i)], True, colourWhite)
                    mytft.screen.blit(text_surface, (textAnchorX, textAnchorY))
                    textAnchorY+=textYoffset
                    text_surface = fontSm.render(forecaseLows[int(i)], True, colourWhite)
                    mytft.screen.blit(text_surface, (textAnchorX, textAnchorY))
                    textAnchorY+=textYoffset
                    text_surface = fontSm.render(forecastPrecips[int(i)], True, colourWhite)
                    mytft.screen.blit(text_surface, (textAnchorX, textAnchorY))
                    textAnchorY+=textYoffset
                    text_surface = fontSm.render(forecastWinds[int(i)], True, colourWhite)
                    mytft.screen.blit(text_surface, (textAnchorX, textAnchorY))
                    textAnchorX+=textXoffset

                # refresh the screen with all the changes
                pygame.display.update()

                # Wait
                time.sleep(updateRate)

if __name__ == "__main__":
    daemon = MyDaemon('/tmp/PiTFTWeather.pid', stdout='/tmp/PiTFTWeather.log', stderr='/tmp/PiTFTWeatherErr.log')
    if len(sys.argv) == 2:
        if 'start' == sys.argv[1]:
            syslog.syslog(syslog.LOG_INFO, "Starting")
            daemon.start()
        elif 'stop' == sys.argv[1]:
            syslog.syslog(syslog.LOG_INFO, "Stopping")
            daemon.stop()
        elif 'restart' == sys.argv[1]:
            syslog.syslog(syslog.LOG_INFO, "Restarting")
            daemon.restart()
        else:
            print "Unknown command"
            sys.exit(2)
        sys.exit(0)
    else:
        print "usage: %s start|stop|restart" % sys.argv[0]
        sys.exit(2)

10 thoughts on “Raspberry Pi PiTFT Weather Station

  1. Very Cool! One of the best projects I’ve seen that makes good use of the PiTFT display. Took me a while to figure out where the files go and get things configured. I also had to change to Imperial units. I switched to the color icons from DeviantArt. Any reason you choose B&W?

    • Hi Tim.

      Just preferred the black & white ones I guess. Glad you found it useful and thanks for the comment.

      Jamie

  2. Quick tip: Use
    logo = pygame.image.load(icon).convert_alpha()
    for the image. It blends better with the background.

  3. Pingback: Prayer Times Display using Raspberry Pi and PiTFT | Baba AweSam

  4. Pingback: Prayer Times Display using Raspberry Pi | Baba AweSam

Leave a Reply

Your email address will not be published. Required fields are marked *