Adding a rules language to Philips Hue lighting automation

Gareth Cronin
5 min readJun 11, 2022

--

In my last story, I explained how I’d wired together a JavaScript rules engine (JSON-Rules-Engine) and a client for the Philips Hue bridge RESTful API to handle complex rules for my home lighting setup. The Github repo is here.

It’s running on a Raspberry Pi and working well, but as I added more rules, I found the native JavaScript object format for the engine was getting unwieldy. Three rules run to 100 lines and the way of expressing conditions is error prone. Here’s an example of just one rule:

I decided to implement a more natural way to express the conditions and events. It makes the rules easier to write and troubleshoot, and so terse that the 32 lines above become a single line:

{hour gte 20,<Living room> none,[Kitchen night light] off} [Kitchen night light] on

I toyed with the idea of creating a fluid API in JavaScript, but I decided to go the whole hog and create a domain-specific language (DSL) with a parser generator. It’s been at least 20 years since I last wrote a context-free grammar: back in the glory days of undergraduate computer science! Back then we used Java, and I seem to recall a very heavy Java port of a clunky old tokenizer and a separate generator to generate Alpha assembly for a Pascal-like grammar. I assumed there must be a more… er… modern approach.

Peg.js

A quick Google turned up Peg.js, a JavaScript parser generator. It runs in JavaScript and produces vanilla JavaScript as output. It combines elementary tokenization with a grammar in the input, which keeps the definition tidy.

The target for my parser is to produce a chunk of JavaScript containing the objects that the JSON rules engine requires. The Peg.js grammar definition is reasonably orthodox, although the choice of / as the “or” operator rather than | seems a bit quirky and caught me out! The tokenization is a bit odd in that it has separate constructs for character matching and strings which return arrays of characters for the former. A basic regular expression that always returns a string would be a lot easier to manage. But hey — it’s open source and it works!

My grammar is pretty simple: each rule is a list of comma-separated conditions inside curly braces, followed by the definition of an event that is fired if the conditions are met.

The conditions are one of these:

  • light or room followed by an on or off state, e.g. [Lounge light] on or <Living room> all
  • the property of a light (e.g. brightness) along with an operator and value, e.g. [Lounge light] brightness gt 20
  • a time-based fact along with an operator and value, e.g. hour lte 21

The events are then one of these:

  • a light followed by a state, e.g. [Lounge light] on
  • a light followed by a property and a value, e.g. [Lounge light] colorTemp 120

The full grammar looks like this:

Note the .join() calls to put the character arrays back together.

Also note a gotcha on tokenization that got me. The order of the list of operators is important. The tokenizer matches on the first match it finds. If gt is listed earlier than gte it will aways match on gt and never on gte leading to syntax errors.

Language design

A simple set of rules don’t deserve too much agonising over design, but I made a couple of conscious choices.

One choice is how closely to map the grammar to the rules engine schema. A close mapping makes it easier to write the code to convert it, but it limits the flexibility that could be harnessed to make a more readable language. I went with a close mapping: the constructs in the grammar reflect the structure of the schema, where each rule entity (a list of conditions, with operators and values, and a single event) is represented. I did make one simplification based on what I ended up using for my own rules. I am only supporting an “all” list of conditions, not the “any” list that is available in the rules engine. I find it easier to model “any” conditions with separate rules.

The other choice I considered was the trade-off between general purpose and domain specificity. In my original implementation, I’d generalised lights and rooms into named facts and kept their properties open. Given the very limited set of attributes available though, it made sense to just build “brightness” and “colorTemp” into the language and create different constructs for a light and a room. This also means I can hide the rules engine “path” property and refer to properties of lights directly.

Sunrise and sunset

While I was at it, I decided to add support for conditions based on local sunrise and sunset. The API at sunrise-sunset.org provides a clean way to get the sunrise and sunset times for a day, given a latitude and longitude. I added the lat/long as settings to my config file and added facts called sinceSunrise and sinceSunset. I found it easiest to map them as an integer number of minutes relative to the actual time. The condition for 30 minutes after sunset is then sinceSunset gte 30and 30 minutes before sunset is sinceSunset gt -30.

I could add more natural terms to the grammar to be able to build clauses like “30 minutes after sunset”, but I’d need to start thinking a bit harder about the language design!

Putting it together

Parsing and data conversion are perfect for unit testing, so I took a test-first approach with Jest and tested that my parser could parse out the right elements. I then took the same test-first approach to build a converter that could take the parsed output and marshal it into the required object structure for the rules engine.

Putting it together

Next steps

As much as I enjoy wielding vi on my Pi, it might be time to add a simple online rules editor so I can change the rules on the fly.

--

--

Gareth Cronin
Gareth Cronin

Written by Gareth Cronin

Technology leader in Auckland, New Zealand: start-up founder, father of two, maker of t-shirts and small software products

No responses yet