Smart Home Project – HomeKit and LightwaveRF Integration

15 Nov

CyberMatters is a blog about security.  This article is NOT about security, there is a related security point related to this article documented in the blog Smart Home Project – Network Segregation

This blog is written to provide more a more technical details to readers that want to integrate LightwaveRF switches (lights) and thermostats with Apple HomeKit as described in the security discussion.

Some reader experience of Linux, package installation and NodeJS is assumed. I started with no NodeJS experience, but after two days, and lots of Googling figured enough out to get something working.  This blog is my way of thanking the many contributors of the blogs and help groups on LightwaveRF and NodeJS , I read to get this far, by making my own small contribution to the debate.

The initialisation steps…

  1. Build a Rapsberry PI. I used the headless Raspbian Jessie Lite OS.
  2. Configure SSL etc
  3. Install NodeJS.
  4. Install HAP.
  5. Configure HAP to build a HomeKit with the default/test accessories, and connect to your phone (Security:  Please change the default password in bridge.js)

For Switches…

In the HAP folder, edit accessories/Light_accessory.js modify the setPowerOn function as follows:

var LWon = ",!R4D1F1";
var LWoff = ",!R4D1F0";
var LWID = 100;    // documentation says this should be a different number each time, so I just increment it.
const dgram = require('dgram');
const server = dgram.createSocket('udp4');

setPowerOn: function(on) {
   console.log("Turning the switch %s", on ? "on" : "off");
   FAKE_LIGHT.powerOn = on; 

   if (LWID++ > 990 ) { LWID = 100 } ;

   if (on) {
      message = new Buffer( LWID + LWon + "|Switch|On\n");
   } else {
      message = new Buffer( LWID + LWoff + "|Switch|Off\n");
   }

   var client = dgram.createSocket("udp4");
   client.send(message, 0, message.length, 9760, "192.168.10.10", function(err, bytes) { client.close(); });
}

The format of the magic string created in the message buffer is best described in this OpenRemote article.

While this allows you to change the switch from on to off, the major missing feature is you cannot tell the current switch status.  As far as I can tell, LightwaveRF simply does not report this status. Googling suggests many others  have struggled with this too.  If you come across a solution please let me know.

However, thermostats do report their status so…

For Thermostats…

This is harder, and in two stages.

 

download

First, edit accessories/Thermostat_accessory.js and modify the “function”:

const LWset = ",!R4DhF*tP";
const LWserial = "A1B2B3"; // The SN of the specific device being controlled
var LWID = 100;

var execute = function(accessory,characteristic,value){
   if (characteristic == "Target Temperature") {

      // Manage race condition - the need for this next 4 lines of code is explained later in the blog.
      var fs = require('fs');
      var fname = "/Temperatures/" + LWserial + ".txt";
      var dname = path.join(__dirname, fname);
      if (fs.existsSync(dname)) { fs.unlinkSync(dname); }

      if (LWID++ > 990 ) { LWID = 100 } ;   //3 digit code, so wrap

      message = new Buffer( LWID + LWset + value + ".0|" + LWserial + "|Set\n");
      var client = dgram.createSocket("udp4");
      client.send(message, 0, message.length, 9760, "192.168.10.10", function(err, bytes) { client.close(); });
   };
}

At this point you’ll be able to set the temperature.  This is very similar to the switch code, but the buffer format is slightly different.

In the configuration at the bottom of accessories/Thermostat_accessory.js I chose to configure the designedMinValue to 3 and designedMaxValue to 3 in both CURRENTHEATINGCOOLING_CTYPE and TARGETHEATINGCOOLING_CTYPE.  This seemed to disable the off/heat/cool/auto mode switches in HomeKit which confused things for me in the iPhone App, and I was not really sure how to deal with each of the values – what to they mean in this context – so I turned it off.  I’m not sure this is the proper way to achieve this, could not find any relevant documentation, but it seems to work.

Getting HomeKit to display the temperature.

This is the second part of the configuration.

I DO NOT CLAIM THIS IS A GOOD SOLUTION.  A NodeJS expert will probably offer a much better approach – but this worked for me! If you are an expert and are able to offer a view – please leave comments below.

Run a separate NodeJS script as a UDP listener, capture the JSON packets from the broadcast, and write them to a file, based on the device serial number:

var path = require('path');
const dgram = require('dgram');
const server = dgram.createSocket('udp4');
var fs = require('fs');

server.on('message', (msg, rinfo) => {
     temp = JSON.parse (msg.slice(2));
     var fname = "/Temperatures/" + temp.serial + ".txt";
     var dname = path.join(__dirname, fname);
     fs.writeFile(dname , msg.slice(2));
});

server.on('listening', () => {
  var address = server.address();
  console.log(`server listening ${address.address}:${address.port}`);
});

server.bind(9761);  // The LightwaveRF broadcast address

Then in bridge.js, I set a timer to run every 5 seconds and iterate through the accessories:

function SensorLoop () {
        accessories.forEach(function(accessory) {
                CheckTemp (accessory);
        });
}

setInterval (SensorLoop, 5000);

Then in CheckTemp() look to if there is a file that matches the device serial number. If there is parse the file, read the cTemp and call setValue() function:

function CheckTemp (LWsensor) {

        var fname = "/Temperatures/" ;
        var dname = path.join(__dirname, fname);

        var LWserial = LWsensor.services[0].characteristics[3].value;
        var tfile = dname + LWserial + ".txt";

        if (fs.existsSync(tfile)) {

                var obj = JSON.parse(fs.readFileSync(tfile, 'utf8'));

                var cTemp = LWsensor.services[1].characteristics[3];
                var cTarg = LWsensor.services[1].characteristics[4];

                if (cTemp.value != obj.cTemp) {
                        cTemp.setValue (obj.cTemp);
                }
                if (cTarg.value != obj.cTarg) {
                        cTarg.setValue (obj.cTarg);
                }
        }
}

The last two lines of code set the target temperature, incase something outside of HomeKit changes the thermostat (e.g., the LightwaveRF app).

This has the potential to create a race condition where the HomeKit user and Timer run at the same time, and the timer overrides the user set value. I choose to manage this this by deleting the file containing the temperature from the device just before setting the temperature from HomeKit (see code in block 2 above).

There is almost certainly a better way – you should be able to run the UDP listener in Bridge.js, and set the values directly, avoiding the file handling and race conditions. When I tried my NodeJS skills were not good enough to make this all work asynchronously, and the bridge stopped working after UDP Packets arrived. Suggestions welcome.

The Finishing Touch…

The JSON in the UDP response from the Thermostat also reports on the battery level.  In a new daily timer, I checked this value and send a message to my phone (via WhatsApp – see here for a how-to) when the level falls below 2.4 (a number arrived at via trial and error).

One Final Point…

One item I have not really solved yet.   If you send too many commands, too quickly, to the LightwaveRF hub, it chooses to ignore them.  So creating sequences of actions, like turning on multiple switches at once in HomeKit goes wrong.    Also, the temperature slider in HomeKit can cause several messages to be sent.
What I’ve not figured out is how to queue the messages (I did try adding a <code>sleep()</code>, but this breaks the asynchronous nature of the Homebridge, and the HomeKit app gets upset.  Suggestions?

If you made it this far, I hope you have found this article of use, in which case please take time to hit the like button below.

Finally, if you made it this far, you are probably trying to do something similar in you home – so please take time to understand the security aspects, both in the article that triggered this blog, and my more general points about running a smart home.

DISCLAIMER. The code and methodology presented here is about getting something to work, it does not necessarily represent good programming practice.

Please join the discussion, we welcome your views...

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: