Saturday 5 February 2022

Making a home thermostat with a Raspberry Pi and the Raku Programming Language. Part 1: getting the temperature.

A few weeks ago I had a moment of hubris on Twitter

I was particularly interested in the idea of making a thermostat with a Raspberry Pi but using Raku instead of Python. Being somewhat impatient and impulsive I bought all the parts I thought I would need from Amazon and set about thinking about what the software would need to do.

The digital thermometer used (the DS18B20 from Maxim Integrated,) seemed to be the natural place to start so I wrote a Raku module to read the temperatures from one or more of the devices. As this may be more generally useful in another programme and I can leave you with a somewhat useful programme without all the other parts, we'll start with this part.

The DS18B20 is a digital thermometer that uses the Dallas 1-Wire interface, the nature of the interface means that a number of the devices can be attached using only a single GPIO pin on the Raspberry Pi. The 1-Wire protocol in general and thermometers specifically are fairly well supported by the Raspberry Pi OS, presenting each attached device as nodes in the sysfs

Assuming you have the RPi::Device::DS18B20 installed on your Raspberry Pi you will only need one or more DS18B20 devices (I'm using the encapsulated "waterproof" ones with longer wires, it seems it's just as cheap to buy five of these from Amazon than the bare chips,) a 4.7K resistor and for convenience a solderless breadboard and some patch cables.

The circuit itself is really quite simple:

Additional thermometers can be added by providing the power and linking the data lines (the yellow wire.)

With this circuit you can run the synopsis example:

And you should get some output like:

28-012113620c31:	20.562
28-03213194ea4f:	17.25

I have two thermometers attached. The first thing you've probably noticed is the "name" of the devices, this device ID is basically baked in the chip at manufacture and not very human friendly so we'll providing a mapping of these to more useful names as we build the application.

For the first part of this we'll create a web application with Cro::HTTP which displays the temperatures dynamically, and for this kind of purpose RPi::Device::DS18B20 provides an asynchronous interface via a which provides the readings periodically:

This emits a Reading object for each attached device periodically, because the thermometers may take different times to produce a result they may not be in any particular order but this means that, unlike the first example, you don't have to wait for a preceding reading which is taking longer. This is ideal for a web application where we can use Server Sent Events with EventSource::Server and some javascript on the client side to update the web page when new reading become available.

Given that we already have a Supply from the RPi::Device::DS18B20 and the EventSource::Server can take a Supply of the events to be emitted we can do the majority of the work by mapping one Supply to one suitable to pass to the EventSource::Server:

Here we are mapping the Reading objects emitted by the RPi::Device::DS18B20 Supply to a custom class TemperatureEvent which contains the mapped name of the device, the device id and the temperature. This class does the role JSON::Class so the objects can be trivially serialised to JSON in the next step where the JSON representation of the object is mapped into a EventSource::Server::Event with a type of 'reading'.

Here I've used a plain Hash to lookup the user friendly name from the device id,for a larger programme it may be more suitable in some configuration or other storage. If you do have more than thermometer device the easiest way to derive the lookup is to apply some source of warmth to each device in turn (a hot liquid or even holding it in your hand,) while running the first example each time, noting the device id with the raised temperature (and possibly labelling for future reference.)

And that is basically that, all that remains is to provide the out-supply of the EventSource::Server object as the content of an appropriate route in the Cro::HTTP application and provide a web-page in which to display the readings and we're done:

The index.html is simply:

Which basically arranges to read the events from the stream and update the table as appropriate. It could obviously be a bit prettier but we'll leave that for later.

In the next post or posts we'll update this application to do the other part of the thermostat application which is to turn relays on and off in order to control the central heating, but as I'm taking a slightly different approach than the post that inspired this I'll need to write a library to drive an MCP23017 first.

The code of the example can be found on Github