Thursday, 28 February 2019

Whose Turn Is it? An OpenHAB Hack (part 1)

As my young family grows up, we have our little routines - one of which is the weekly Movie Night. On a rotating basis, each family-member gets to choose the movie that we'll watch, as a family, on a Saturday night. Looking at other screens is not allowed during this time - it's a Compulsory Family Fun Night if you like. The thing is, maybe I'm getting too old, but it frequently seems very difficult to remember whose turn it is. Maybe we skipped a week due to some other activity, or nobody can remember exactly because it was a group decision. Anyway, something that computers are especially good at is remembering things, so I decided to extend my existing OpenHAB home (device) automation to include home process automation too!

Unlike the similarly-named Amazon Alexa "skill" which appears to a) be totally random and b) not actually work very well, I wanted something that would intelligently rotate the "turn" on a given schedule (weekly being my primary requirement). I also wanted to keep the essentials running locally, on the Raspberry Pi that runs my OpenHAB setup. I'm sure you could move this entirely into the cloud should you wish, but doing it this way has allowed me to start with the basics and scale up.

First step; create a simple text file with one participant name per line: ${OPENHAB_USERDATA}/movienight.txt (i.e. /var/lib/openhab2/movienight.txt on my system):

Charlotte
Mummy
Daddy
Sophie
Make sure that the openhab user can read and write it.

Now we use the exec binding to create a Thing that reads the first line of this file via the head command-line tool, once every 6 hours (21600 seconds). Unfortunately as you'll see in all the snippets below, there seems to be no way to access environment variables when defining these file locations; so while I'd love to write ${OPENHAB_USERDATA}/movienight.txt, I have to use the hard-coded path: /var/lib/openhab2/movienight.txt.

$OPENHAB_CONF/things/householdrota.things:

Thing exec:command:movienight "Movie Night" @ "Living Room" 
  [command="head -1 /var/lib/openhab2/movienight.txt", 
   interval=21600,
   timeout=5, 
   autorun=true
]

Here are the items that fetch, display and adjust the current movie night, respectively. It's useful to be able to adjust the rotation, for example if we skipped a week, so need to back out the automatically-changed value.

$OPENHAB_CONF/items/householdrota.items:
Switch FetchMovieNight {channel="exec:command:movienight:run"}

String MovieNight "Whose turn is it?" 
  {channel="exec:command:movienight:output"}

Switch AdjustMovieNight

We expose the items in the sitemap:

$OPENHAB_CONF/sitemaps/default.sitemap:
  ...
  Frame label="Household rotas" {
    Text item=MovieNight label="Whose Movie Night is it?"
    Switch item=AdjustMovieNight
           label="Adjust Movie Night"
           mappings=[ON="Rotate", OFF="Unrotate"]
  }
  ...
Which results in the following in Basic UI:

Now for the weekly-rotation part. First, a simple Bash script to rotate the lines of a text file such as the one above. That is, after running ./rotate.sh movienight.txt, the topmost line becomes the bottom-most:

Mummy
Daddy
Sophie
Charlotte
/home/pi/rotate.sh:
#!/bin/bash

TMPFILE=$(mktemp)
if [[ $# -eq 2 ]] 
then
        # Assume a -r flag provided: Reverse mode
        TAIL=`tail -n 1 $2`
        echo ${TAIL} > $TMPFILE
        head -n -1 $2 >> $TMPFILE 
        mv $TMPFILE $2
else
        HEAD=`head -1 $1`
        tail -n +2 $1 > $TMPFILE 
        echo ${HEAD} >> $TMPFILE
        mv $TMPFILE $1
fi

And now we can automate it using a time-based rule in OpenHAB - each Saturday night at 9pm, as well as supporting rotation "by hand":


$OPENHAB_CONF/rules/householdrota.rules:
rule "Rotate Movie Night - weekly"
when
  Time cron "0 0 21 ? * SAT *"    
then 
  logInfo("cron", "Rotating movie night...")
  executeCommandLine(
    "/home/pi/rotate.sh /var/lib/openhab2/movienight.txt"
  )
  FetchMovieNight.sendCommand(ON);
end


rule "Adjust Movie Night"
when
  Item AdjustMovieNight received command
then 

  val reverseFlag = if (receivedCommand == ON) "" else "-r"

  val results = executeCommandLine(
    "/home/pi/rotate.sh " + 
    reverseFlag + 
    " /var/lib/openhab2/movienight.txt", 5000)

  # If anything went wrong it will be displayed in the log:
  logInfo("AdjustMovieNight", "Results: " + results)
  FetchMovieNight.sendCommand(ON);
end

Now this is fine, but believe me when I tell you that having a text field available in a web page somewhere is simply not enough to achieve a winning SAF (Spousal Acceptance Factor). So onwards we must plunge into being able to ask the Google Home whose turn it is ...