[<< | Prev | Index | Next | >>] Friday, April 12, 2024
Home Sweat Home
[Home automation]
Hackaday is running a little contest that seemed a good fit and excuse for me to describe my home automation system, which I've been meaning to do anyway, so here it is. (I don't expect to win and don't even know what the prizes are. More interested in potential collaborators.)
What started with my dishwasher dying evolved into a new home automation library and infrastructure that makes it super quick to create and integrate new devices--including custom hardware.
I'll describe here everything that's automated in my home and show how they're all elegantly tied together, from dishwasher controller, thermostat, RO water valve, wifi temperature sensors for freezers and such (all custom hardware costing less than $100 combined) to zwave and Shelly devices, to radon sensors and geiger counter, to controlling the TV and stereo, to voice commands (hosted in-house), automated reminders and so on.
The core of the system on the software side is a (python) library that makes it easy to publish and find attributes by name and type. If you have a widget with a couple of switches, a light, and a dial, you simply publish four values to the home-automation network: two read-only booleans (the switches), a read/write boolean (the light), and a read-only real (the dial). By convention, these are named in a path hierarchy, like "widget/switch1", "widget/dial", and so on.
The library takes care of all the dynamical aspects. When someone turns the dial, your (driver) code just calls dial.set_value(v), and that's it. If nobody is currently listening to the dial, nothing happens. If one or more tasks anywhere on the network are listening (whether in the same application, or on a different machine entirely), they are notified (once!) that the value has changed. When and if they care to know, they ask for the latest value and get it. This is a little slower in terms of latency (on a scale irrelevant to humans) but wildly more efficient than the usual approaches in terms of total bandwidth, flow management, and all that practical crud that usually makes this stuff a headache. And again, all of this happens in the library--neither end has to worry about it.
If you wanted that widget to control your bedroom lamp brightness, the code would look like this--and you could run this code on any machine on the network, regardless of where the code managing the widget or lamp were running (but it could all be in the same process just as easily):
lamp = ha.find("master_bedroom/north_lamp") dial = ha.find("widget/dial") async for value in dial: await lamp.send_value(value)That for loop runs forever, quietly waiting for the dial to be turned, and then sets the lamp accordingly. If the dial generates five values in the time it takes the lamp to receive and acknowledge the value, four of them are never even sent over the network (because they went stale before anyone needed them!). And to be clear, the "async for" automatically sets up a subscription to the dial's value stream in the background, and if you exit the for loop, the subscription is closed.
Note it doesn't matter here whether the lamp or dial are on zwave, Shelly, custom, or whatever. The above works even if the lamp or dial don't even exist at the time the code is run: the async for will wait for the dial to come online. If you smashed the (zwave, say) widget and replaced it with a new custom device with new code on the driver side, the above loop doesn't even need to be restarted -- it will simply wait while you're changing the devices, and resume when the (new) widget/dial comes online.
Of course the interface to the dynamic values is much richer than just async iterators, but it's all pretty straightforward and obvious like that.
It would be very easy to add a nice GUI layer for a code-free experience, such as being able to simply visually connect the "widget/dial" port to the "master_bedroom/north_lamp" port via a "wire" or whatever. And that could run as a separate process with no need to modify any of the existing code (wouldn't even require restarting any processes besides the GUI itself). But so far it's been so easy to code anything I've wanted I haven't needed that personally.
I did write a "monitor" which is like a hierarchical file browser for the entire HA network. It's less than 1000 lines of python code and provides a curses-based interface which updates in real time as well as letting you set/change any settable values, graph historical plots, and so on. This is a totally generic browser, non-specific to my particular network or devices. Here's a snapshot of mine right now with a few things randomly opened (the ">" on the left show where the hierarchy can be opened). Note this is just an administrative tool, not really intended to be a primary UI, but it does work as one when needed:
Again, all of this is updating in real time. If I want to turn on my Yamaha stereo amplifier, I just go to the "power" line and select True. If I go to the "audio_effect" property, a menu with all the available audio effects appears. To make that work, the driver code for the amplifier simply publishes an additional informational dictionary about the audio_effect variable, under the path "yamp/audio_effect/info", which lists the options for that value. (Everywhere you see "info" above, it is the info dict for the parent object.) Even that can update dynamically though that's rarely needed.
As with any system like this, you need to write a little wrapper code for each new type of device, but that amounts simply to creating a dynamic variable object for each value you wish to publish, and then reading/writing it as needed. The dynamic vars look roughly the same on either end of the pipe, so the code in the "drivers" looks more or less like the code snippet above, plus whatever is truly specific to the device. (The main difference is just that the owner of a variable "sets" it whereas everyone else "sends" to it. And that the owner can receive "sends", either via callback or await style. Sends can even raise exceptions, asynchronously, and they propagate back over the network to the calling async send. By convention those are end-user friendly exceptions like "You can't set the brightness above 100" which means any high-level GUI can give useful feedback to users about any device on the network. And incidentally the dynamic variables work great within the same process as well and don't needlessly marshal the values or anything like that, let alone call any network code, so it's quite common for a driver to have tasks awaiting its own values and such.)
I've written a generic Zwave device wrapper. The "zooz4in1_2" in the monitor screenshot is a zwave device. If I open the "values" group there it would bring up all the other zwave attributes available for that sensor besides the key ones shown there.
"Yamp" above is a Yamaha Amplifier with an ethernet port in the back. They publish the protocol and it was fairly easy to write a wrapper for it. Likewise for the LGTV (WebOS) and so on. In general the philosophy is: create a low-level wrapper around the device which exposes the functionality of the device in the most straightforward way possible (that wrapper should have no dependencies on this home-automation project at all--something you could use anywhere), then write another wrapper/bridge that publishes that API as a bunch of dynamic values (or occasionally calls, which are also allowed), and then later and elsewhere write any actual control code strictly in the generic terms of networked dynamic values, as in the trivial (but real and sufficient) code snippet above. The device wrappers are typically in the same process, but the control code is typically separate. This means it's easy to restart/rewrite particular devices or sub-systems at any time without impacting the rest of the system. (E.g., I can launch the monitor any time from any machine, have many running at once, and so on. Any change made from any monitor of course shows up in all the others in real time. Also my main "AI", or automation and scripting module, is just one process that I can tweak and restart freely without, say, having to recycle the zwave network or whatever.)
One feature of this overall approach is that it's really easy to make and integrate custom hardware. For instance I made a bunch of temperature sensors simply by jumpering a $4 ESP8266 dev board to an MCP9808, and writing a tiny bit of driver code that waits for simple ascii commands in UDP packets and replies. If it's gets "temp c" it replies (to the sending IP/port) with the current temperature in Celsius. If it gets "leds on" it turns the dev board's LEDs on. That was all I needed to do, and I've cloned that combo multiple times to make wifi freezer sensors, a remote sensor for my home thermostat, and so on.
The low-level python driver code just wraps up that simple UDP protocol as a couple of calls to query or set the temp and leds, and the next level wrapper just publishes those as dynamic values on the HA network. It's a lot of pieces but they're all quite simple, and presto: some new values appear in the monitor.
This meant it was trivial, for instance, to connect a motion sensor in my bedroom to the LEDs on the remote temperature sensor (two totally unrelated devices) so that when I get up in the middle of the night, two tiny blue LEDs turn on, which is just enough light so I don't kill myself on whatever landmine my kids left for me. (The flow is: zwave motion sensor -> zwave wrapper -> generic motion event (over TCP) -> trivial code that throws a switch for 30 seconds in response to an event -> generic boolean value (over TCP) -> temp sensor wrapper -> UDP -> ESP8266. But that's all set up with a few lines of code for that trivial bit in the middle.) Notably, the high level code (like the monitor, or a wire-up GUI, or a scripting front end) doesn't need to know that the LEDs are part of a temperature sensor, or even that they're LEDs -- they're first and foremost just a boolean value, which can be used anywhere a boolean value can be.
The dishwasher and thermostat are similar, just with a bit more logic on the device itself so that they run reasonably independently even if the HA system is offline. (As an aside, I did two revisions of the thermostat--one in a rush with push-on jumpers in a bug box, and then I went ahead and had a board made for version 2 -- five devices fully assembled for under $100 delivered to my door which is pretty crazy! It works great but I would make some changes next time. If anyone wants to collab on a very-affordable wifi/bluetooth thermostat let me know...)
Version 1:
Version 2:
The dishwasher controller:
With the same pattern, I was able to integrate my geiger counter (one of the first Onyx devices) in a couple of hours, which is fun because:
With a couple lines of code, any dynamic variable can be tracked over time in a database, and published on the network as a historical stream, which the monitor can plot. Here is my current air quality plot (some traces lag because the Airthings Waves batch by hour):
The vertical bands in the Temperatures plot shows where my furnace was on. They are blue when the A/C is on. The dashed red line shows the current heating target temperature. Note the target temp changes gradually in ramps rather than sudden jumps. (It's so easy to script up when you can just write a little python to tweak some dynamic values. The thermostat controller reads a temperature schedule from a simple text file, and updates the target temperature ramps any time that file is changed. So I just edit that text file as needed or swap it for different ones as the seasons change. Plus I have a "bump" feature that lets us bump the temperature up or down when we're chilly or hot, and that offset gets gradually eroded by the scheduled ramps, so we can tweak the temp when we need without screwing up the schedule. That combined with the remote sensor in our bedroom has made our house so much more comfortable than our old stock thermostat, every single day, that it was well worth the time to make.)
Inspired by the ease of making new devices, I cobbled together a quick water valve for our RO system so I can turn it on for a given amount of time (here shown with the cover plate off):
Because it's easy to run processes on different machines, I am able to run speech recognition (faster_whisper) on my desktop machine (while most of the HA stuff runs on a small box that doubles as a router/firewall), and to send the results to the command channel (which is also connected to Telegram), so now I can push a microphone button on my phone and say "fill a gallon" and the RO water will turn on just long enough to fill a gallon jug of water. (The valve box shown above just accepts a command to run for a certain number of seconds and then shuts off, so even if the network goes down it will not keep running.)
Most of the important HA features are available in a Telegram chat with my house as a menu of buttons (like turning on/off the bedroom lamps, raising/lowering the thermostat heat, etc.) But the speech recognition turns out to be especially good for parameterized things like "set the [tv/stereo] volume to 35" or "run the water for two minutes twenty seconds" (filling my humidifiers, etc). I can of course type the same things to the same Telegram channel.
And having all the different devices controlled together is of course great for "scenes" such as, after my toddlers have twiddled all the buttons and dials on the TV and stereo such that nothing is working at all, I can say (verbally) "start Netflix" and the stereo and TV are appropriately re-configured, volumes set, lamps down, and so on. But that's pretty standard home automation stuff.
Our current Telegram menu:
The Telegram bot gives us a bunch of other commands and controls as well, which are great when we're away (only accessible from our accounts of course). And again, it was very easy to integrate because it works in terms of vanilla dynamic values with no concern for where they're coming from.
I also wrote a simple Android app which turns an old Android phone into a remote camera. From the monitor (the same vanilla monitor above) I can select front/back camera, resolution, focus modes, and so on. Any app on the network can fetch an image from any camera at any time, and I use that to make motion-timelapse security videos. Here are the two pointing out the front of the house atm:
I think it would be fun to do a timelapse from each camera at exact solar noon every day. Haven't done it yet but it should just be a few lines of code when I get around to it.
I have some USB webcams which are published the same way as the android cams, so my full security camera panel is a mixture of source types--which the code that displays them is completely unaware of. Because of the push/pull nature of the library, it's an automatic feature that when I launch the viewer on my laptop or wherever, the driver on the box with the USB camera opens the camera and starts recording. The same thing happens if I simply open the camera hierarchy in the monitor down to the level of the raw jpeg stream (at which point I start seeing a succession of the first few byte of jpeg encoded images in the monitor). When the last consumer closes, so does the camera. Again these are all separate processes on different machines.
For anything I want running regularly, I add it to the process manager for that machine, which makes it available as a process in the monitor. This allows me to quit and restart processes (say for a code update) from the monitor without even having to remember which machine they're running on. Also makes it easy to see the running status of every process/driver on the network regardless of where it's hosted. (On my todo list is to make a red/green status panel that just shows at a glance that everything is running as expected, with the ability to drill down into red items to find out what's wrong, restart processes if needed, and so on. But so far everything has been so robust, it all "just works" and I haven't had to worry about this.)
If there is interest, I would be happy to open source everything. My sense is it would be in competition with Home Assistant and such, which are so extensive now I doubt it's worth the trouble, which is why I haven't done so already. But there it is.
[<< | Prev | Index | Next | >>]