---
title: 'CatBus: design'
subtitle: a home automation platform built on MQTT
date: 2020-02-20
---
{% extends 'templates/base.html' %}
{% block body %}
{{ title }}
{{ subtitle }}
{% markdown %}
Home automation and the "Internet of Things" (IoT) is so hot right now.
Between the phone vendors, washing machine makers, and light-bulb pushers, there are many home automation platforms.
In this document, I will try to explain why I have made my own [15th standard](https://xkcd.com/927/).
contents…
[TOC]
## Why not `$platform` ?
I initially started by using Apple HomeKit via [HomeBridge](https://homebridge.io), which has a large collection of plugins for many of my devices.
However, it was always limited to being controlled from Apple devices, and the automation abilities were limited.
I briefly looked at [Home Assistant](https://www.home-assistant.io), which is open-source and can be controlled from Linux, but seemed brittle and inflexible.
When I was gifted some hardware that lacked plugins for either HomeBridge or Home Assistant, requiring me to write software to use them, I decided I would invest that effort into building my own platform.
## Wants & needs
### Why do I want home automation?
To lead a "better" life.
### What do I mean by "better"?
To lead a less stressful life:
- Automate things I forget to do.
- Turn down the lights as it gets late for better sleep.
- Turn up the lights before the alarm goes off for an easier wake.
- Detect when I'm at home, and remind me to eat.
To lead a comfier life:
- Convenience!
- Actionable metrics, e.g. to predict whether or not I will feel cold in a given outfit given room temperatures.
## Practical considerations
### Low maintenance
This is a hobby project, so it must be low-effort to maintain:
- The system must be easy to add new parts to.
- The system must allow existing parts to run without ongoing intervention.
- The system should support multiple parts acting in concert without reconfiguration.
### The real world comes first
The system will not "own" the devices it controls: the TV's own remote must continue to work.
- The system must not interfere with external control systems.
- The system should reflect changes caused by external control systems.
### Frictionless interaction
When I used HomeKit, a frequent problem was that I had to use an Apple product to use it.
If I had left my phone charging instead of in my pocket, I had to go and get it, which grew to be an annoyance.
It should thus be controllable without needing a specific item:
- The system must have a command-line interface.
- The system must have a web interface.
- The system should have a HomeKit interface.
- The system should have physical controllers.
## Models of operation
Now that I have my requirements, there are a few high-level models of operation for managing the distributed state.
First, a quick glossary:
- A **user** is a human interacting with the system, either directly via the software or indirectly by being inside the house.
- An **observer** is a part of the system that observes the state of something external to the system, such as the temperature of a room or the volume of a speaker.
- An **actuator** is a part of the system that affects the state of something external to the system, such as the channel of a TV or whether a light is on.
In practice, many actuators are also observers.
- An actuator has one or more **controls**, usually one for each thing that it affects, such as one for a TV's volume level and another for its channel.
- A **controller** is a part of the system that tells the actuators what to do using their controls, such as a web UI or an automation daemon.
### Remote objects
_Remote objects_ (RPC, REST, SOAP, …) is a common idiom in distributed computing.
A sender sends a message to recipient, which actions the message, possibly returning a response.
The sender must know where to send the message, or talk to a central router that itself must know where to send the message.
Under this system, to set up a new actuator for an existing control, I would need to add the actuator to the control's configuration.
This is more wiring and configuration than I can be bothered with, and contravenes the [low maintenance requirement](#low-maintenance).
### Intent-based
In an _intent-based_ control system there is a central intended system state, and one or more actuators that try to make it reality.
For example, if the intent says that the lights should be on, an actuator daemon continually checks the actual state of the lights, turning them on if they are off.
This allows for low coupling, as the controller that sets the intent doesn't care which actuator turns on the light and how, which meets the [low maintenance requirement](#low-maintenance).
However a naïve implementation risks violating the [real world requirement](#the-real-world-comes-first), e.g. if a light is turned off at the device itself, the actuator will repeatedly try to turn it back on.
### The compromise: semi-intent-based
The current implementation uses [MQTT](https://mqtt.org/) as a sort of real-time database.
MQTT has "topics", and clients can publish events to topics and subscribe to events from topics.
It meets the [low maintenance requirement](#low-maintenance):
- Controllers & actuators only need to directly communicate with the MQTT Broker.
- Controllers & actuators only need to know what topics they will subscribe or publish to.
- Multiple actuators can listen on the same topic, which allows for "complex" devices:
- For example, "turn the speakers on" can trigger a relay device to physically turn the speakers on, and trigger another device to redirect its audio output.
To meet the [real world requirement](#the-real-world-comes-first), actuators only act when a topic's value changes.
The two rules for actuators are:
- When a topic is updated, the actuator tries to make it true.
- When an actuator notices an external change in the real-world state, it puts the new value on the bus.
For example, with one actuator, one observer, and an external control:
1. The TV is off.
2. The topic `home/living-room/tv/power` is set to `on`.
3. The actuator receives this change of state, and turns the TV on.
4. After some time, the TV is turned off with the remote.
5. The observer notices the TV is off, and sets the topic `home/living-room/tv/power` to `off`.
6. The actuator receives this change of state, and tries to turn the TV off, which it already is.
An example with two actuators:
1. The speakers are off.
2. The topic `home/living-room/speakers/power` is set to `on`.
3. The first actuator receives this change of state, and turns the amp on.
4. The second actuator receives this change of state, tries to turn the receiver on, and fails.
5. The second actuator sets the topic `home/living-room/speakers/power` to `off`.
6. The first actuator receives this change of state, and turns the amp back off.
## Scheme
MQTT is similar to a simple key-value store, with strings as keys and unstructured bytes as values.
The keys can have hierarchy, like a filesystem path, but this isn't required.
As it is stringly typed, I have opted for a key naming scheme:
```
home/{zone}/{device}/{control}(/{metadata})?
```
Similar to the [Prometheus best practices](https://prometheus.io/docs/practices/naming/), topics have `_suffixes` that state a topic's units and valid values.
For example, a topic `home/kitchen/speakers/volume_percent` can only have number values in the range `[0,100]`.
Since not all types of control are so convenient, topics can have optional metadata topics, e.g. a topic `…/foo_enum` can have a metadata topic `…/foo_enum/values` to list valid values for controllers to choose from.
topic | data
------------------------------------------------- | ----
`home/living-room/speakers/power` | `on`
`home/living-room/speakers/input_enum` | `UPnP`
`home/living-room/speakers/input_enum/values` | `UPnP\nAirPlay`
`home/outside/weather-sensor/temperature_celcius` | `15`
The hierarchy also allows for controllers presenting a unified device to a user, even if it is actually multiple devices:
- `speakers/power` is controlled by an Energenie RF relay socket.
- `speakers/input_enum` is controlled by Snapcast.
- `speakers/volume_percent` is controlled by ALSA.
{% endmarkdown %}
{% endblock %}