Thursday 29 October 2020

PyBloom coding project part 6: Setting the colours

 

We're into part 6 of this project, and I figure out how to set the colours of the Philips Hue lamps. Of course it's more complicated than I first thought.

Hue lamp object

This is the module that accesses the colour light bulbs, Philips Hue Bloom, and changes the colour according to the temperature observation. The observations are accessed from the persistent SQLite database. The module then makes use of a Python library that wraps the Hue API, and a couple of utilities that do the maths to convert colours expressed as CSS hex values into xy values that the Hue lamps understand.

The technologies 

  • Philips Hue API

  • Qhue library

  • Rgbxy library

  • SQLite database

The code

Initialisation


class hue_lamp:


    def __init__(self, lamp_id):

        # not accessible

        ip = HUE_IP

        username = HUE_USERNAME


Instantiating a hue_lamp object creates the accessors (getters and setters) for the lamp colours. These first two variables within the object are only needed during initialisation, in order to set up the connection to the Hue Bridge, so aren't made available in an object method.


Time for a detour to get to know the Hue API. Philips encourages the hacker community and has comprehensive developer documentation on its website, which walks through setup. Whilst the API is a RESTful interface, you don’t actually send messages to the cloud. Instead, you communicate with the Hue Bridge, which is somewhere in your internal network, so there are none of the security issues of the big bad internet. 


self.bridge = qhue.Bridge(ip, username)


The Bridge is identified uniquely by its IP address. It recognises your username as trustworthy, and allows access to its methods. How it decides you are trustworthy is simple and elegant - when you set up the username on the Bridge, you have to press a button on the Bridge at the same time, therefore proving that you are in possession of it and that it’s in the same internal network.


self.getter = self.bridge.lights[lamp_id]()

self.setter = self.bridge.lights[lamp_id]


The actual data object was a little difficult to get my head around, and it took a bit of trial and error to get the syntax for the getter and setter methods right. The getter is a function that returns a Python dictionary of all the resources associated with the bulb. This dictionary is created by Qhue from the JSON returned in the API. Here’s an example JSON response:


{

    "state": {

        "hue": 50000,

        "on": true,

        "effect": "none",

        "alert": "none",

        "bri": 200,

        "sat": 200,

        "ct": 500,

        "xy": [0.5, 0.5],

        "reachable": true,

        "colormode": "hs"

    },

    "type": "Living Colors",

    "name": "LC 1",

    "modelid": "LC0015",

    "swversion": "1.0.3"

}


The setter is different as it reflects the difference in the API. The API allows sending a PUT message to the URL of the root of the bulb; you don’t have to access the URL of the actual setting.


self.is_on = self.getter['state']['on']

self.colour = self.getter['state']['xy']

self.name = self.getter['name']


The final step in initialisation is to make a few of the statuses easier to access. Whether the light is on or not is a boolean within the state dictionary, which is itself within the returned dictionary. The colour (in xy values) is a list within the same dictionary. To access a dictionary within a dictionary you can simply chain the key names. By contrast, the name is a string in the returned dictionary, so no chaining is needed.


Representation string


def __str__(self):

    if self.is_on:

        status = 'on and is set to xy:' + str(self.colour)


As usual, I’ve written a representation string to aid in debugging. If the light is on, we have access to information on the state and colour.


else:

    status = 'off'


If the light is off, we don’t know its colour, so the status information is different.


return f'{self.name} is {status}'


The return string uses my favourite f-string Python function to construct the message.


Setter methods


def turn_on(self):

    self.setter.state(on=True)

    return 'Lamp turned on'


def turn_off(self):

    self.setter.state(on=False)

    return 'Lamp turned off'


These first two methods simplify the (already quite simple) way to to turn lights on and off, to make it more readable.


def set_colour(self, colour):  # colour is a tuple of xy values

    self.setter.state(on=True)

    self.setter.state(xy=colour)

    return 'Lamp changed colour'


This final setter method first turns on the lamp if it isn’t already on, then sets its colour. Perhaps obviously, if the lamp is off, changing its colour alone will not make the bulb show the colour; the state has to be set to on as well.


The colours lookup table


At its core, there is a lookup table that converts temperature to colour. Since this colour is to be shown both on the Hue lamps and on the accompanying website, I decided to store this table externally. I had experience of using csv files for this purpose, but I decided to use SQLite. Both are supported natively by Python, but SQLite has some built-in methods which will be useful later on. This first utility function pulls this lookup table from the external database.


def lookup_colour(temp):


This helper function takes in a temperature value, and will return a corresponding rgb hex value.


sql = 'WHERE temperature = (?)'

what = (temp, )  # tuple with single item

results = get_rows('colours', 'hex_value', rows_sql=sql, args=what)


We now use the second utility function that accesses our database. It’s quite a flexible function that handles opening and closing the connection to the SQLite database, and creating the SQLite query from filter criteria. Later on, we’ll get into the detail of how this utility is created, but for now, let’s see why this particular query was constructed.


The SQLite query we’re trying to construct is:


SELECT ‘hex_value’

FROM ‘colours’

WHERE temperature = temp


  • hex_value is the column which contains the data we’re interested in

  • colors is the table that contains all the data

  • temperature is the row which we want to filter

  • = temp is the condition we want to apply to the row


Under the hood, the utility function makes use of the SQLite .execute(sql, args_opt) method. If the SQLite query contains one or more (?), SQLite3 replaces the ? with the arguments from the second half of the method. This is a neat way of passing parameters from Python to SQLite. 


There are a fair few syntax wrinkles to keep track of. The SQLite must be a string, the ? within that string must be enclosed by brackets, and the arguments must be presented as a tuple. In this case, we’re passing a single parameter, the temperature. Because it’s a tuple of a single value, we need the trailing comma to make it work - it’s one of those Python things that tripped me up the first time I came across it.


return results[0][0]  # to return the hex value string only


The function returns a table as a list of tuples, where each tuple is a row. In this case, we’re getting a table of a single value [(hex_string, )]. Both lists and tuples are iterable, so we can chain index values to access the string directly. It looks awkward, though.


Calculating the temperature to look up


def find_temp_threshold(temp):

    rows = get_rows('colours', 'temperature')

    all_thresholds = [row['temperature'] for row in rows]


First, let’s get all the temperature thresholds from our external table using our utility database access function. As we saw in the previous section, the result is a table comprising of a list of dictionaries. Each value can be accessed using an index or by its column label. In the last function, I used the index to access the value for brevity but in this function I use the label as it’s more readable. From the database setup (described later), we know that this is an integer which means we can use normal list methods.


max_threshold = max(all_thresholds)

min_threshold = min(all_thresholds)


Finding the maximum and minimum threshold temperatures are now easy to find.


Calculating the colour


As mentioned earlier, we can’t just pass the CSS hex value of the colour we want to the Hue lamp and expect it to understand. To get the web site to talk to the bulb, we need to convert rgb hex values to xy coordinates. 


temp_threshold = int(temp/5) * 5

if temp_threshold > max_threshold:

    temp_threshold = max_threshold

if temp_threshold < min_threshold:

    temp_threshold = min_threshold

return temp_threshold


The algorithm does the following:

  1. Round the temperature down to the nearest multiple of 5

  2. If the temperature is higher than 40, return the higher bound temperature which is 40

  3. If the temperature is lower than -10, return the lower bound temperature which is -10


The resulting temperature table looks like this:


Temperature range (celsius)

Corresponding colour (hex value)

40 or greater

ff0000

Between 35 and 40

ff4000

Between 30 and 35

ff8000

Between 25 and 30

ffbf00

Between 20 and 25

ffff00

Between 15 and 20

bfff00

Between 10 and 15

00ff80

Between 5 and 10

00ffbf

Between 0 and 5

00ffff

Between -5 and 0

00bfff

Between -5 and -10

0080ff

Lower than -10

0040ff


def convert_temp_to_colour(temp):

    temp_threshold = find_temp_threshold(temp)


Having used our little helper function to calculate the temperature threshold for our lookup table, we now use a couple of modules from the rgbxy library to do the maths of the conversion from rgb hex to xy coordinate. I’ve previously touched on why this maths is complicated, but let’s recap. The hex value is used in CSS. Each pair of hex digits is the value for each of red, green and blue. So the first entry in the table for temperatures of 40C or greater shows maximum red (ff) and no green or blue (00 each). Note that rgb values do not have any concept of saturation (brightness), so maximum saturation is assumed. So the colour for 40C is bright maximum red.


converter = Converter(GamutA)


Each Hue bulb has its own colour gamut, the range of colours that it can reproduce (i.e. colour space constrained by its physical limitations). This first statement sets the applicable colour gamut to the one supported by my Hue Blooms, in this case gamut A.


colour = converter.hex_to_xy(lookup_colour(temp_threshold))

return colour


There’s a few nested things going on here, so let’s unpack them.

  • We found the temp_threshold using the algorithm described above

  • The lookup_colour function fetches the corresponding hex value from the lookup table in the external database table

  • We use the .hex_to_xy method from the converter library to do the maths

  • And because we set the gamut already, the method ensures that resulting colour is within the colour space of the Bloom lamp


Identifying the lamps


When we set colours and status of the Hue lamps, we need to be sure we’re doing so to the correct lamps. The Hue Bridge maintains a representation of all of the items (bulbs, lamps, switches, sensors etc) attached to it, and each item is assigned an ID. These are created when the item is provisioned


hue_lamp_ids = {

    'den bloom': 10,

    'lounge bloom': 11

}


This dictionary is another lookup table that maps IDs to human-readable names, to make the code easier to follow. I got the numbers by taking a look at the JSON from the Bridge, using the web tool provided by Philips. You can find it on the development page.

Putting it together

class hue_lamp:


    def __init__(self, lamp_id):

        # not accessible

        ip = HUE_IP

        username = HUE_USERNAME

        # accessible

        self.bridge = qhue.Bridge(ip, username)

        self.getter = self.bridge.lights[lamp_id]()

        self.setter = self.bridge.lights[lamp_id]

        self.is_on = self.getter['state']['on']

        self.colour = self.getter['state']['xy']

        self.name = self.getter['name']


    def __str__(self):

        if self.is_on:

            status = 'on and is set to xy:' + str(self.colour)

        else:

            status = 'off'

        return f'{self.name} is {status}'


    def turn_on(self):

        self.setter.state(on=True)

        return 'Lamp turned on'


    def turn_off(self):

        self.setter.state(on=False)

        return 'Lamp turned off'


    def set_colour(self, colour):  # colour is a tuple of xy values

        self.setter.state(on=True)

        self.setter.state(xy=colour)

        return 'Lamp changed colour'



def lookup_colour(temperature):

    # lookup table of temperatures to colours in database.sqlite3

    sql = 'WHERE temperature = (?)'

    what = (temperature, )  # tuple with single item

    results = get_rows('colours', 'hex_value', rows_sql=sql, args=what)

    return results[0][0]  # to return the hex value string only



def find_temp_threshold(temp):

    # find max and min thresholds from external database

    rows = get_rows('colours', 'temperature')


    all_thresholds = [row['temperature'] for row in rows]

    max_threshold = max(all_thresholds)

    min_threshold = min(all_thresholds)


    temp_threshold = int(temp/5) * 5

    if temp_threshold > max_threshold:

        temp_threshold = max_threshold

    if temp_threshold < min_threshold:

        temp_threshold = min_threshold

    return temp_threshold



def convert_temp_to_colour(temp):

    temp_threshold = find_temp_threshold(temp)

    converter = Converter(GamutA)

    colour = converter.hex_to_xy(lookup_colour(temp_threshold))

    return colour



hue_lamp_ids = {

    'den bloom': 10,

    'lounge bloom': 11

}


Part 6 has taken us through the second main module and how to work the the Philips Hue API. In part 7, I'll take you through how to create pretty graphs from the data, the fundamentals of data visualisation. Also visit https://github.com/Schmoiger/pybloom for the full story.




No comments:

Post a Comment

It's always great to hear what you think. Please leave a comment, and start a conversation!