Wednesday 28 August 2013

Part 3 - Writing a Philips Hue Simulator in Javascript - Implementing the lights API

In part 2, I talked about the routing strategy and we added the functionality to authenticate the requesting user against the bridge "whitelist". I've been "offline" for a while (due to disrupted internet access as part of house renovations) but I'll try and pick things back up where I left off. In this post, we'll implement some of the features provided by the lights section of the API.

Let there be lights...


Before we start implementing the query functions, it makes sense to set up a model for our light (bulb) hardware, a bit like we did with the users in the previous post. We'll create a lights.js in the models subdirectory:
var fileSystem = require('fs');

var lights = [];
var fileName = "./lights.json"

exports.getLights = function () {
    var data = {};

    for (var i = 1; i <= lights.length; i++) {
        data[i.toString()] = {
            "name": lights[i - 1].name
        };
    }

    return data;
};

function loadLights() {
    fileSystem.readFile(fileName, function (err, data) {
        lights = JSON.parse(data);
        console.log("LoadedLights = " + lights);
    });
};

loadLights();
This gives us a nice abstraction to our modelled light hardware data which will come in useful if we decide to store it in a slightly more practical form than just a simple JSON file - anyway, for now we'll just create a simple lights.json file that will be populated with our test data and read by our lights model code:

[{
    "1": {
        "state": {
            "on": false,
            "bri": 200,
            "hue": 0,
            "sat": 0,
            "xy": [0.0, 0.0],
            "ct": 153,
            "alert": "none",
            "effect": "none",
            "colormode": "",
            "reachable": true
        },
        "type": "Test light bulb #1",
        "name": "TestLightBulb1",
        "modelid": "123456",
        "swversion": "1.2.3.45"
    }
}]


Now to implement the API functions that query this information from the bridge. Update routes.js to add the specific function route - the whole file should now look like this:
function routes(users) {

    var lightsController = require('./controllers/lights');
    var groupsController = require('./controllers/groups');
    var schedulesController = require('./controllers/schedules');
    var configController = require('./controllers/configuration');
    var portalController = require('./controllers/portal');

    var hasUsernameProperty = function (request) {
        return request && request.params && request.params.hasOwnProperty("username");
    }

    var validateUser = function (request, response, next) {
        if (hasUsernameProperty(request)) {
            next();
        }
    };

    var authenticateUser = function (request, response, next) {
        if (hasUsernameProperty(request) && users.checkUser(request.params.username)) {
            next();
        } else {
            response.send(200, [{
                error: {
                    type: 1,
                    address: '/',
                    description: 'unauthorized user'
                }
            }]);
        }
    };

    // Lights
    app.get('/api/:username/lights', authenticateUser, lightsController.getAllLights);

    // Configuration
    app.post('/api/:username', validateUser, configController.register(users));
}

module.exports = routes;
A key addition is the authenticateUser function - this will automatically check the user is present in the whitelist and emit an error if they're not. Remember in my previous post, we implemented functionality to be able to add a user to the whitelist - this mechanism simply enforces that. In order to complete the request handling, we'll add our corresponding controller function to actually handle the request. Our lights.js in the controllers subdirectory will now look like this:
var lights = require('../models/lights');

exports.getAllLights = function (request, response) {
    response.send(200, lights.getLights());
};
Now we have a representative, end-to-end implementation of our first light API function. To test it - use curl to send the relevant url request:

C:\>curl -is http://localhost:8080/api/loada/lights

This should return all lights that are known to the bridge in the same format as we'd expect from the real bridge hardware:

C:\>curl -is http://localhost:8080/api/loada/lights
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 31
Date: Tue, 16 Jul 2013 10:49:21 GMT
Connection: keep-alive

{"1":{"name":"TestLightBulb1"}}

Now we have a vertical slice through the simulator - request handling, authentication, data retrieval and response - we can implement the other light API methods. The next method to implement - to retrieve all new lights - requires a bit of extra work because we really need to implement the function that will search for the new lights in order to be able to query them. So, we'll skip ahead to that - add this line to our routes.js file (under the existing route lines):
app.post('/api/:username/lights', authenticateUser, lightsController.searchForNewLights);
Then modify the lights.js in the controllers subdirectory to add:
exports.searchForNewLights = function (request, response) {
    lights.searchLights();
    response.send(200, [{"success": {"/lights": "Searching for new devices" } }]);
};
This requires us to implement the corresponding model function - add this to the lights.js in the models subdirectory:
exports.searchLights = function() {
 if (scanTimeoutId !== null) {
  clearTimeout(scanTimeoutId);
  isScanInProgress = false;
  scanTimeoutId = null;  
 }

 isScanInProgress = true;
 lastScan = new Date();
 
 scanTimeoutId = setTimeout(function() { 
  lights.forEach(function(light) {
   // TODO - only discover 15 lights per search
   if (light.discoveredOn === null) {
    light.discoveredOn = lastScan;
   }
  });
 
  isScanInProgress = false;
 }, 60000);
}
Note that we need to add these local variables to the top of the file:
var lastScan = null;
var scanTimeoutId = null;
var isScanInProgress = false;
They allow us to keep track of the current search so that the simulator can respond correctly when asked for any new lights. In order for the functionality to work, I've added a variable to the light in our JSON model (lights.json):

"discoveredOn": null


To determine the difference between a "new" light and one the bridge already knows about, this field's value is checked - if it's null, it's new. I've added some code to automatically mark all our lights in our model as discovered at startup for now although the actual bridge may do something slightly different (I haven't checked) - the updated loadLights function now looks like this:
function loadLights() {
 fileSystem.readFile(fileName, function(err, data) {
  lights = JSON.parse(data);
  console.log("LoadedLightsCount = " + lights.length);
  
  // Mark each light as "discovered"...
  var scanned = new Date();
  
  lights.forEach(function(light) {   
   light.discoveredOn = scanned;
  });
 });
};
It feels like I've got a bit bogged down here in the nitty gritty of the low-level bridge behaviour - anyway, here's the new lights query function that we were going to implement before we went off piste. As before, we add the route to routes.js:
app.get('/api/:username/lights/new', authenticateUser, lightsController.getNewLights);
We then add our controller function to the lights.js in the controller subdirectory:
exports.getNewLights = function (request, response) {
    response.send(200, lights.getNewLights());
};
And finally, we add the model function that will do the actual work into the lights.js file in our models subdirectory:
exports.getNewLights = function() {
 var lightsQuery = {};

 lights.forEach(function(light, i) {
  if (light.discoveredOn === lastScan) {
   lightsQuery[(i + 1).toString()] = { "name": light.name };
  }
 });
 
 lightsQuery.lastscan = isScanInProgress ? "active" :
         lastScan === null ? "none" : 
         lastScan.toString();
 
 return lightsQuery;
}

I'll freely admit this is a bit rough-and-ready and the logic is a bit ropey but I wanted to keep things fluid and keep progress going forward.

Next time, I'll spend a little time looking at how we can automate some tests for things which should come in useful for checking we're implementing the API correctly. Oh, and continue to flesh out the API implementation in the code of course...