Weewx+Raspberry PI+HDMI+PyGame

I’ve been using wview with my WH1080 weather station for some time (actually 2 of them). My main setup has been using my server, and every now and again the WH1080 would seem to lock up and nothing could get data out of it. The solution was to drop it’s power, on reboot it would all start working again.

However wview also seemed to introduce lockups of it’s own and the only solution there was to reboot the server (not ideal). So when it came to setting up a second weather station (in a remote location) I needed something a bit more stable and started looking at alternatives. I was doing this on a Raspberry PI and found wview. After installing it sometime last year it seemed pretty stable (although the WH1080 still manages to lockup).

Back to my house and I’ve finally had enough of missing weather data. One thing I really liked with wview was the ability to pull the archive data if the weather station had been running when the machine hadn’t (providing the USB hadn’t locked up), I really recommend looking at wview if your starting out.

I’ve already covered setting up weewx on a Raspberry PI and I’m not going to post about my exact configuration here. Instead I’m going to share my Python code for displaying the various graphs and gauges straight to the TV. At the moment I have my weather PI connected to the TV via HDMI. This may change later and then I’ll have to adjust the code to pull the images to another pi before displaying them (I have a similar project for displaying webcams already).

So a few things before the code.

  1. It’s my first real attempt at using classes, so my code will be more than a little scattered.
  2. You need to have python, pygame, wview, (and for this exact code Bootstrap for wview but you could just change the file paths to the Standard guages and graphs), mysql and wview configured for mysql.
  3. I have this started using an init script (added below).
  4. This runs the python program as root, I need to find a way to run this as a normal user (but that will affect point 5&6).
  5. This program checks mysql is able to be connected to and restarts mysql of not.
  6. It also checks the freshness of the index.html file (not the best way but a quick way) to make sure wview is running keeping the files upto date. If not it reboot the PI, this causes the weather station to reboot so if the USB locks up the whole system resets fixing it.

So now onto the python code

#!/usr/bin/python
import os
import time
import pygame
import MySQLdb as mdb
import signal
import sys

imglocation = "/var/www/weewx/Bootstrap"

class pyscreen :
   screen = None;

   def __init__(self):
       "Initializes a new pygame screen using the framebuffer"
       disp_no = os.getenv("DISPLAY")
       if disp_no:
           print "I'm running under X display = {0}".format(disp_no)

       # Check which framebuffer drivers are available.
       drivers = ['fbcon', 'directfb', 'svgalib']
       found = False
       for driver in drivers:
           # Make sure that SDL_VIDEODRIVER is set
           if not os.getenv('SDL_VIDEODRIVER'):
               os.putenv('SDL_VIDEODRIVER', driver)
           try:
               pygame.display.init()
           except pygame.error:
               print 'Driver: {0} failed.'.format(driver)
               continue
           found = True
           break

       if not found:
           raise Exception('No suitable video driver found!')

       size = (pygame.display.Info().current_w, pygame.display.Info().current_h)
       print "Framebuffer size: %d x %d" % (size[0], size[1])
       self.screen = pygame.display.set_mode(size, pygame.FULLSCREEN)
       pygame.mouse.set_visible(False)
       # 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."

   def size(self):
       size = (pygame.display.Info().current_w, pygame.display.Info().current_h)
       return size

   def fill(self, colour):
       if self.screen.get_at((0,0)) != colour:
           self.screen.fill((0, 0, 0))
           self.screen.fill(colour)
           pygame.display.update()

   def image(self, img, locX, locY, sizX, sizY):
       try:
           if (( "week" not in img) and (os.stat(img).st_mtime > time.time() - 600)):
               # 600 = 10 mins.
               image = pygame.image.load(img)
               image = pygame.transform.scale(image, (sizX, sizY))
               self.screen.blit(image, (locX, locY))
           elif(( "week" in img) and (os.stat(img).st_mtime > time.time() - 7200)):
               # 7200 = 2 hours.
               image = pygame.image.load(img)
               image = pygame.transform.scale(image, (sizX, sizY))
               self.screen.blit(image, (locX, locY))
           else:
               pygame.draw.rect(self.screen, (255, 0, 0), (locX, locY, sizX, sizY), 0)
       except pygame.error, message:
           pygame.draw.rect(self.screen, (255, 0, 0), (locX, locY, sizX, sizY), 0)
       pygame.display.update()

   def text_object(self, msg, font):
       black = (0, 0, 0)
       textSurface = font.render(msg, True, black)
       return textSurface, textSurface.get_rect()

   def error(self, msg):
       largeText = pygame.font.Font('freesansbold.ttf', 115)
       TextSurf, TextRect = screeny.text_object(msg, largeText)
       size = screeny.size
       TextRect.center = ((pygame.display.Info().current_w/2),(pygame.display.Info().current_h/2))
       self.screen.blit(TextSurf, TextRect)

       pygame.display.update()

class fileman:
   global imglocation

   def __init__(self):
       "Init for fileman. Nothing to do atm."

   def __del__(self):
       "Destructor for fileman. Nothing to do again."

   def total_files(self):
       count=0
       for file in os.listdir(imglocation):
           if file.endswith(".png"):
               count=count+1
       return count

class sqly:
   def __init__(self):
       "do nothing"

   def test(self):
       try:
           con = mdb.connect('localhost', 'weewx', 'weewx', 'weather')

           cur = con.cursor()
           cur.execute("SELECT VERSION()")
           ver = cur.fetchone()
           return 0
       except mdb.Error, e:
           return 1

       finally:
            "do nothing"
   def restart(self):
       #wait 30 secs and try the connection again.
       time.sleep(30)
       if not mysql_con.test():
           try:
               os.system("service mysql start")
           except:
               "do nothing"

def sigterm_handler(_signo, _stack_frame):
    "When sysvinit send the TERM signal, cleanup before exiting"
    print("[" + get_now() + "] received signal {}, exiting...".format(_signo))
    sys.exit(0)

signal.signal(signal.SIGTERM, sigterm_handler)

def reboot():
    "check if we've been reboot in the last 30 mins"
    uptimef = open("/proc/uptime", "r")
    uptimestr = uptimef.read()
    uptimelst = uptimestr.split()
    uptimef.close()

    if float(uptimelst[0]) < 1800:
        "We've reboot in the last 30 mins, ignoring"
    else:
        try:
            os.system("reboot")
        except:
            "do nothing"


if __name__ == "__main__":
    try:
        screeny = pyscreen()
        filey = fileman()
        screeny.fill((0, 0, 255))
        size = screeny.size()
        border = 7
        sizewquarter = ((size[0]-(border*5))/4)
        sizehthird = ((size[1]-(border*4))/3)

        print("Total files : %d" % (filey.total_files()))
        while 1:
            mysql_con = sqly()
            if mysql_con.test():
                screeny.fill((250, 0, 0))
                screeny.error("Database Offline. Restarting!")
                mysql_con.restart()
            elif (os.stat("/var/www/weewx/Bootstrap/index.html").st_mtime < time.time() - 600):
                screeny.fill((254, 0, 0))
                screeny.error("NOT Updating. Reboot Imminent!")
                reboot()
            else:
                screeny.fill((0, 0, 255))
                screeny.image("/var/www/weewx/Bootstrap/barometerGauge.png", (border*1), (border*1), sizewquarter, sizehthird)
                screeny.image("/var/www/weewx/Bootstrap/outTempGauge.png", ((border*2)+(sizewquarter*1)), (border*1), sizewquarter, sizehthird)
                screeny.image("/var/www/weewx/Bootstrap/windDirGauge.png", ((border*3)+(sizewquarter*2)), (border*1), sizewquarter, sizehthird)
                screeny.image("/var/www/weewx/Bootstrap/windSpeedGauge.png", ((border*4)+(sizewquarter*3)), (border*1), sizewquarter, sizehthird)
                screeny.image("/var/www/weewx/Bootstrap/big_images/weekbarometer-Bootstrap.png", (border*1), ((border*2)+(sizehthird*1)), ((sizewquarter*2)+(border*1)), sizehthird)
                screeny.image("/var/www/weewx/Bootstrap/big_images/weektempchill-Bootstrap.png", (border*1), ((border*3)+(sizehthird*2)), ((sizewquarter*2)+(border*1)), sizehthird)
                screeny.image("/var/www/weewx/Bootstrap/big_images/weekwinddir-Bootstrap.png", ((border*3)+(sizewquarter*2)), ((border*2)+(sizehthird*1)), ((sizewquarter*2)+(border*1)), sizehthird)
                screeny.image("/var/www/weewx/Bootstrap/big_images/weekwind-Bootstrap.png", ((border*3)+(sizewquarter*2)), ((border*3)+(sizehthird*2)), ((sizewquarter*2)+(border*1)), sizehthird)
            time.sleep(1)
    except KeyboardInterrupt:
        "We've got an interupt"

and now the init.d code

#!/bin/sh
#
# init script for displayweewx
#

### BEGIN INIT INFO
# Provides:          displayweewx
# Required-Start:    $syslog $network
# Required-Stop:     $syslog $network
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: init script to display weewx charts via HDMI output
# Description:       The python script queries mysql and file ages, so does not rely on mysql as a backup way to kick it.
### END INIT INFO

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
NAME=displayweewx
DAEMON=/root/displayweewx/main.py
DAEMONARGS=""
PIDFILE=/var/run/$NAME.pid
LOGFILE=/var/log/$NAME.log

. /lib/lsb/init-functions

test -f $DAEMON || exit 0

case "$1" in
    start)
        start-stop-daemon --start --background \
            --pidfile $PIDFILE --make-pidfile --startas /bin/bash \
            -- -c "exec stdbuf -oL -eL $DAEMON $DAEMONARGS > $LOGFILE 2>&1"
        log_end_msg $?
        ;;
    stop)
        start-stop-daemon --stop --pidfile $PIDFILE
        log_end_msg $?
        rm -f $PIDFILE
        ;;
    restart)
        $0 stop
        $0 start
        ;;
    status)
        start-stop-daemon --status --pidfile $PIDFILE
        log_end_msg $?
        ;;
    *)
        echo "Usage: $0 {start|stop|restart|status}"
        exit 2
        ;;
esac

exit 0

After all of this we get the following on the TV

From far far away.
From far far away.

Raspberry PI + Maplin WH1080 Weatherstation

Download and write to SD Card the debian image for Raspberry PI.
Boot and connect via SSH (Putty from Windows)
Change to root using sudo su – alternative use sudo in front of commands. Using sudo su – is bad, but I always do it.

Run

apt-get update
apt-get upgrade

Download the latest Debian Weewx version (http://sourceforge.net/projects/weewx/files/) using wget. Then run

dpkg -i weewx_2.6.4-1_all.deb

This will most likely throw error errors about missing dependencies. Install them using apt-get install

dpkg -i weewx_2.6.4.1_all.deb
(Reading database ... 75409 files and directories currently installed.)
Preparing to replace weewx 2.6.4-1 (using weewx_2.6.4.1_all.deb) ...
Unpacking replacement weewx ...
dpkg: dependency problems prevent configuration of weewx:
 weewx depends on python-configobj (>= 4.5); however:
  Package python-configobj is not installed.
 weewx depends on python-cheetah (>= 2.0); however:
  Package python-cheetah is not installed.
 weewx depends on python-imaging (>= 1.1.6); however:
  Package python-imaging is not installed.
 weewx depends on python-usb (>= 0.4); however:
  Package python-usb is not installed.
dpkg: error processing weewx (--install):
 dependency problems - leaving unconfigured
Errors were encountered while processing:
 weewx

So in my case I run
apt-get install python-configobj python-cheetah python-imaging python-usb
During the install you’ll be asked several configuration questions, fill them in (You can always edit the config file later if you make a mistake).

For the maplin WH1080, Select the FineOffsetUSB.

Once installed weewx should attempt to start (if not you can start it with /etc/init.d/weewx start). Check the syslog for any errors
tail /var/log/syslog -n 50
If all goes well you should see something like

wxengine: Initializing weewx version 2.6.4


wxengine: Using Python 2.7.3 (default, Mar 18 2014, 05:13:23) #012[GCC 4.6.3]


wxengine: pid file is /var/run/weewx.pid


wxengine: Using configuration file /etc/weewx/weewx.conf


wxengine: Loading station type FineOffsetUSB (weewx.drivers.fousb)


fousb: driver version is 1.6


fousb: polling mode is PERIODIC


fousb: polling interval is 60


fousb: altitude is 4.2672 meters


fousb: pressure offset is 0.0


fousb: found station on USB bus=001 device=005


wxengine: StdConvert target unit is 0x1


wxengine: Record generation will be attempted in 'hardware'


wxengine: The archive interval in the configuration file (300) does not match the station hardware interval (60).


wxengine: Using archive interval of 60 seconds


archive: Created and initialized table 'archive' in database 'weewx.sdb'


wxengine: Using archive database: archive_sqlite


stats: Created schema for statistical database


stats: stats database up to date.


wxengine: Using stats database: stats_sqlite


wxengine: Starting up weewx version 2.6.4

If you encounter errors you can edit your weewx configuration using nano -w /etc/weex/weewx.conf
Once you’ve finished editing press ctrl+x (to exit), then y(to save), then enter(same filename). Then restart weewx using /etc/init.d/weewx restart
If all has gone well you may also see entries in your syslog like
weewx[12559]: archive: added record 2014-09-20 18:32:44 UTC (1411237964) to database 'weewx.sdb'; table 'archive'
You should also have /var/www/weewx loaded with files.
As we haven’t yet installed a webserver though you can’t view them.
We’ll install Apache2 Server to handle our webpages.
apt-get install apache2
Once apache is installed open the weewx pages by visiting http://{raspberry pi ip address}/weewx from your browser. e.g. http://192.168.1.52/weewx
This setup does leave weewx running as root, not really something you would do for a system running on the internet. but outside the scope of securing your server for here.
Checkout http://www.weewx.com/docs/usersguide.htm#installing for more info on installing, problems and further guides for configuring your weewx installation.
Also checkout http://davies-barnard.co.uk/2013/12/weewx-rasp/ for a better looking skin template (the link is in one of the comments).