Some time ago I developed a home automation server to handle some basic tasks like aggregating all of the family’s calendars and serving up the configuration for the wall tablets that we have around the house. That server was built in Python using Flask and has been extended a few times to add things like Sonos control. The source to all of this (and the new servers described below) is on GitHub.
The server has been running fine for a year or more but unfortunately the last few changes I have made (adding functionality) seem to have introduced an instability that stops Flask responding to requests. I’ve tried hard to track down the problem and have rolled back to earlier versions to make sure I’m not imagining that it used to be reliable. But the problem is that I can only narrow it down by allowing the server to run for a few days as the problem doesn’t surface immediately.
Splitting up the Server
What this has made me realise is that the idea of lumping lots of disparate functionality together in a single server program is probably a bad idea. The Sonos functionality has nothing in common with the serving of tablet configuration so it probably doesn’t make sense to have them served together. I’m no expert in this area though so I’d be happy if someone better informed has any views.
My investigations so far have led me to think of the following organisation:
Function | Tech | Comment |
Wall Tablet Configuration | node.js | This is a really simple function to implement and the configurations are in json so it makes sense to pull this into a separate node.js server |
Calendar Aggregator | node.js | Found a great module called ical for node js https://www.npmjs.org/package/ical developed by Peter Braden @peterbraden who seems like an interesting guy – he also developed a node js wrapper for opencv which I must try sometime! |
Sonos REST API | node.js | I’m planning to move to using the node-sonos-http-api by Jimmy Shimizu (jishi) which I’ve tested and seems to work well |
Alert Manager | ? | This uses Twilio for alerting at the moment but isn’t fully implemented and I suspect there might be a better way? |
Door Controller Watchdog Keep-Alive | node.js | Simply a repeating request made to the door-controller web service which works as a reset to the watchdog timer on the door controller – as described in this blog post. |
Development of node.js config server
First off the configuration server needed to be rebuilt. This is a simple situation but I decided to learn more about node.js and the express web framework – starting with this tutorial.
A very good way to get node to install code is to create a package.json file in the source folder and then run npm update. For example to get a mongodb server connector into the project, create a package.json file containing:
{
"name": "nodeConfigServer",
"version": "0.0.1",
"private": true,
"dependencies": {
"express": "*",
"jade": "*",
"mongodb":"*",
"JSON":"*"
}
}
When npm update is run this causes Express (a node web server), Jade (a templating library to handle HTML generation), MongoDB (no-SQL db) and JSON (json manipulation library) – and al their dependencies – to be added to the node_modules folders under the current folder.
Creating a templated 404 page
Jade handles creation of pages in response to error events and normal operation if required. To create a 404 (not-found) page template simply requires the following to be placed in a file called 404.jade in the folder Views under the main node source folder.
doctype html
body
h1= 'Could not load page: ' + url
Returning configuration for a specific tablet
The bulk of the work of the configuration server is in servicing a single REST API call “/tabletconfig/…”
app.get('/tabletconfig/:devname?', function (req,res) {
queryName = "deviceIP";
queryVal = req.ip;
if (req.params.devname !== undefined)
{
queryName = "deviceName";
queryVal = req.params.devname;
}
queryStr = {};
queryStr[queryName] = queryVal;
mongoDBClient.collection('TabletConfig').findOne(queryStr,
function(err, doc) {
if ((err === null) && (doc !== null))
res.send(doc);
});
});
There’s not much to say about this other than that initially a tablet must have a known IP address but devices can use the API by providing their device name as a parameter. This is partly because tablet IP addresses might be changed by DHCP and also because I intend to use other devices (like my mobile) to access this configuration and it’s IP address will definitely not be static.
Calendar Server Dev
Since the config server proved to be so simple and effective I decided to use the same components for the calendar server. The job of the calendar server is also simple: to access one or more shared calendars (Google, Office 365, etc) and respond to simple queries such as “give me all the events in the next 5 days”.
Unfortunately the format returned for calendars is ICS which is rather horrendous and needs a lot of taming – fortunately this is done by the ical node module. Using that the only remotely tricky part is unwinding the “recurrence rules” that you get with calendar entries and I didn’t want to force this onto other devices that want to consume a simpler data feed.
The server has two parts:
- On a timer (i.e. every half hour or so) load the calendar from the web, convert to json and store in mongodb
- When accessed via the REST API get the calendar from the database, expand out recurring events and return the events within the time range requested
Node ICAL module problem
One problem occurred during development relating to the treatment of recurring events. It seems that sometimes more than one VEVENT record in an ICS feed can have the same UID. The node Ical module makes the assumption that all VEVENT records that have a UID have a unique UID because it stores the json formatted VEVENT records in a javascript object (i.e. a dictionary like structure) and a second VEVENT with the same UID will overwrite the first.
I had to make a change to the ical module’s code to overcome this and ended up forking Peter Braden’s repo. The change I made isn’t great though (I simply store the VEVENTS with different UIDs) and I notice on the forum for the ical module that others have made pull requests on similar changes – and have fixed other bugs.
Anyhow, I found here a way to change the entry for ical in package.json to get npm to load from my fork rather than from the original master.
{
“name”: “nodeConfigServer”,
“version”: “0.0.1”,
“private”: true,
“dependencies”: {
“express”: “*”,
“jade”: “*”,
“mongodb”:”*”,
“JSON”:”*”,
“ical”:”https://github.com/robdobsn/ical.js/tarball/88f30f6f184756c674ce379affdb7024a164043c”,
“rrule”:”*”
}
}
Keep Alive for Door Controller
The final part of this was the simplest – call the REST API for the door controller to keep it alive. The entire code for this is …
http = require("http")
moment = require("moment")
secsBetweenKeepAlive = 60
keepDeviceAlive = ->
http.get("<a href="http://192.168.0.221/status"">http://192.168.0.221/status"</a>, (res) ->
timeStr = moment().format('YYYY-DD-MM HH:mm:ss')
console.log timeStr + " KeepAlive " + if res.statusCode is 200 then "OK" else res.statusCode
setTimeout(keepDeviceAlive, secsBetweenKeepAlive * 1000);
res.on "data", (chunk) ->
#console.log "BODY: " + chunk
return
return
).on "error", (e) ->
console.log "Got error: " + e.message
setTimeout(keepDeviceAlive, secsBetweenKeepAlive * 1000);
return
keepDeviceAlive()
Installation and Execution
Configuration Server
The primary function of the configuration server is to deliver a block of json to each wall tablet which it uses to set up the icons and layout of the pages.
For instance this very simple bit of json defines some tiles to be present in the favourites section (the home page of the tablet software) of the tablet called tabdining (which is in the dining room).
{
"deviceName" : "tabdining",
"favourites" : [
{
"tileName" : "Kitchen Mood 1"
},
{
"tileName" : "Kitchen Off"
},
{
"tileName" : "Kitchen On"
},
{
"tileName" : "Conservatory Off"
},
{
"tileName" : "Conservatory On"
}
]
}
It is so simple because each tile is actually just a copy of a tile that already exists on another page so all that is needed to get it displayed is the tile name.
A more complex example is for a tile that should be included on another page – in this case the “sonosTier”. The json here results in a new tile being created from scratch and given an action which involves automating the Sonos API exposed by the Sonos Server (see below).
{
"deviceName" : "tabdining",
"tileDefs_Sonos":
[
{
"actionUrl": sonosServerUrl + "Office" + "/favorite/BBC%20Radio%204",
"uri": sonosServerUrl + "Office" + "/favorite/BBC%20Radio%204",
"groupName": "Sonos",
"tierName": "sonosTier",
"colSpan": 1,
"rowSpan": 1,
"name": "Radio 4",
"visibility": "all",
"tileType": "Sonos",
"iconName": "music-play"
}
]
}
The “visibility” tag contains “all”, “portrait” or “landscape” to control visibility in different screen orientations.
Sonos Server
I’ve been able to replace all my code for the sonos server (which used the SoCo project started by Rahim Sonawalla) with the fantastic node sonos API by Jimmy Shimizu https://github.com/jishi/node-sonos-http-api
cd /robdev/deploy
git clone <a title="https://github.com/jishi/node-sonos-http-api.git" href="https://github.com/jishi/node-sonos-http-api.git">https://github.com/jishi/node-sonos-http-api.git</a>
cd node-sonos-http-api
npm install
node server.js