Home automation field notes All concepts

health

A house that comments on your blood pressure

An upper-arm cuff, a Bluetooth dongle on a stick that could not keep up, a small board by the bed that could, and a wall display that had to be talked out of looking surprised.

There are two of us in the flat and one blood pressure monitor, a Beurer BM85 upper-arm cuff with two memory slots, M1 and M2 — one each, which is the only way the house ever learns whose arm it was — and a Bluetooth radio that wakes up for about a minute after you press the round button. It came with an app. The app wanted an account. We wanted the numbers on the home server, in a CSV file, with nobody in between. This turned out to be easier than the app.

What we ended up with is a house that reads your arm and then says something about it. Carefully. The saying-something part is where most of the work went, and most of the mistakes.

A cuff that refuses to be paired

The monitor will not pair. Ask it and it answers AuthenticationFailed, politely and every time. It does not need to. Bluetooth Low Energy peripherals only ever advertise; the central end connects, and the cuff hands over its readings to whoever connects during its one-minute window. Under the Beurer badge it is an iHealth board speaking the standard blood pressure service, with a few dialect quirks: the pulse arrives as a 16-bit float with its bytes the wrong way round, and the user slot is a single byte after it, zero-based.

So there is no app and no pairing, only a passive daemon on the home server that listens to the advertising stream the operating system is already scanning anyway. When the cuff's address appears it connects, subscribes, collects everything in the cuff's memory (the device always sends all of it, so readings are de-duplicated by timestamp), writes the CSV, updates a sensor and sends a notification. The daemon runs inside the same container as the house, which means every restart kills it, so an automation revives it at start-up and every ten minutes. It used to be hourly.

A reference range, not a diagnosis

Each reading is classified by a template sensor into the European guideline bands: optimal, normal, high-normal, hypertension grades 1 to 3, and low. The classification uses the higher of systolic and diastolic, so 119/85 lands in high-normal on the strength of the diastolic alone, and 120/90 is grade 1 however pleasant the top number looks. Pulse gets its own bands, from markedly slow through the 60 to 89 that most of us live in, to markedly fast above 120.

From the two bands a sentence is assembled: who, the numbers, the pressure band with a line of context, the pulse band with another. If the cuff flagged movement or an irregular beat, a warning is appended. The lines were written to be useful without being alarming. High-normal is told that this is not yet high, that high starts at 140/90. Grade 1 is told that one reading is not a diagnosis, and that several days of it in a row are worth a doctor's time. Fast pulse is asked whether there was coffee.

This is a reference range applied to one reading by a template. It is not medical advice, not a diagnosis, and not a substitute for a person with a stethoscope. The house says so in its own sentences, and it says so here.

The comment, and where it goes

The comment goes to the phones and to the small display in the living room described on the screen page: the numbers in the big line, the sentence beneath, and a face chosen to match. After sixty seconds the display's own automation is re-triggered and the house spirit — the virtual resident whose mood normally owns that screen — gets its wall back. We did not duplicate any of its logic for this; it already knows how to draw itself. For three hours afterwards the spirit carries the verdict too, quietly pleased at a normal number and worried at a high or low one, which is its own page's business.

The automation triggers on the reading's timestamp, not on its value. Two readings can share a systolic, and a state trigger would have swallowed the second one in silence. There is also a guard against the restart artefact where a sensor goes from nothing to something and looks, for a moment, like news.

Two bugs are worth telling. The first face we sent was the house's general mood of the moment, which happened to be curious: a blue, wide-eyed, faintly startled expression, next to a perfectly normal reading. The face now follows the verdict, and only the verdict: a purpose-drawn measured face for optimal, normal and high-normal, a worried one for any grade of hypertension or for low. A comment about your arteries should not borrow its expression from the weather.

The second bug was a small empty square in front of the pulse — a stethoscope emoji, and the display's fonts have no emoji. The diary and thoughts fields had been filtering them since the first day; the big word field had not, because until then nothing but a mood word had ever gone into it.

The readings that did not arrive

One morning a reading from one of us kept failing to arrive while the other's went straight through. The daemon logs the signal strength of every advertisement it hears, which is the only place in the house where that number exists, so we could measure rather than guess. Three minutes apart, same cuff, same dongle: one reading at -70 dBm from two metres succeeded at the first attempt; the other at -86 dBm from three metres failed twice. Sixteen decibels is not one extra metre of air, which would cost about three and a half decibels. At 2.4 GHz a human body eats ten to twenty, and the difference was a body in the path. The dongle had been sitting behind the server; it is now on an extension, 1.8 metres up, in the open. Honest accounting: the first samples after the move averaged about -73 dBm against -77 before, which is too few readings and too small a gap to call an improvement, so we are collecting a week from the same spot on the sofa before we say anything.

Software was the larger share of the trouble. One evening the daemon heard the cuff at -80 dBm and then died, because stopping the scanner threw No discovery started — the house's own Bluetooth layer shares the adapter and starts and stops scanning as it pleases, so that is a race, not a fault — and the revival was more than half an hour away, fifty-nine minutes in the worst case. The retry loop had a matching flaw: two real timeouts followed by six instant device not found errors, all six inside nine seconds, because once scanning stopped the system forgot the device. Stop, read and start are now guarded separately, each retry begins with a fresh lookup by address and keeps looking for up to 45 seconds, and a failure no longer sits in silence: the daemon sends a notification saying why, with the signal strength, and what to do. At -84 dBm or below, the advice is about line of sight, not distance. The reading itself is never lost; the cuff keeps it and sends it with the next press of the button.

Charts

One chart each, two in all, full width and stacked: pressure and pulse on their own axes, a month at a time, with the guideline lines drawn in so a number has somewhere to stand. The data comes from a small script that turns the CSV into JSON, with the time zone stated explicitly because the container makes no promises about its own. The last point is not extended to the edge of the chart; the default did that, and a week-old reading drawn as a steady plateau is a lie with an axis. The card shows the same red error whether the configuration is wrong or the card has merely not loaded yet, which cost an evening until we checked our configurations against the card's own schema, found them innocent, and pressed refresh harder — verify the outcome, not the configuration, as ever.

The morning nag

One of us is meant to measure every morning. So there is a nag: a window from half past nine to one during which, if nobody has measured and somebody is home and awake and there is no guest, the wall display goes orange and impatient, the bulb blinks, a banner appears across the television if it is on, and a counter climbs (the escalation is what the spirit's nagging face is for, over on its page). At noon, if still nothing, a single message goes to the phones. The whole thing switches off the moment a reading arrives in the house, which is not the same as the moment the cuff is used; the button still has to be pressed.

The nightly reviewer that reads the day's logs — our machine colleague on the night shift — once flagged a suspiciously quiet morning: the nag had not fired, and it wanted to know why. The answer was that a reading had already arrived that morning, before the noon message was due. We now hand the reviewer the reading as evidence alongside the silence, and it has stopped worrying.

The dongle on a stick, and the board by the bed

The dongle on the hub hears the cuff at somewhere between minus seventy and minus eighty-five decibel-milliwatts, which is the radio equivalent of a conversation across a busy restaurant: possible, but you will be asked to repeat things. So we put a Bluetooth proxy by the bed, a thumb-sized ESP32 board running ESPHome, whose entire purpose in life is to hear what the hub cannot and pass it on. It heard the cuff beautifully: three hundred and thirty-nine advertisements in two minutes at minus seventy. The house ignored every one of them.

One owner per address

This is not a bug. It is a rule, and a sensible one for everything in the flat except this cuff. The house's Bluetooth layer keeps one owner per device, whoever hears it first, which is usually the local dongle, because it has no Wi-Fi hop to lose the race by. A challenger takes over only by being sixteen decibels louder. Until then its reports are not deprioritised; they are dropped on the floor before anyone is told. A second rule sits beside it: a repeated advertisement whose contents have not changed is not passed on either, so that beacons saying the same thing four times a second do not become a callback storm. The cuff says exactly the same thing every time. Our listener therefore got one call per appearance, and on the last night, none.

The connection is scored on a different rule again, plain signal strength, so the proxy was in fact being handed the connection while being denied the advertisement, and losing it, because every step of the handshake travelled over Wi-Fi to the server and back and the cuff hangs up on a central that hesitates. Six attempts in one night, six differently worded failures, one cause: we were slow.

A board that reads it itself

On reflection, proxy was the wrong job for the board. The right job was to be the central. The ESP32 now connects to the cuff on its own, with its own radio stack, subscribes to the readings and forwards each one to the house as an event carrying the raw bytes, which the same parser as before turns into numbers. No Wi-Fi in the handshake, no dongle, no rule about owners. The first transfer after the change took six seconds for forty readings, in one attempt, at minus seventy-two. The fourth, unattended, at minus eighty-one, took the same six seconds. The old path had never once managed minus eighty-one.

Two quirks earned lines of their own. The cuff keeps advertising for a minute after a transfer, so a board that reconnects at once starts a second transfer which the cuff's own timer then cuts short, and the cuff shows Err for a job that had in fact succeeded; there is a sixty-second cooldown now. And the board's firmware will not let a connect action live inside the trigger that saw the advertisement, because a reference cannot be stored in a tuple, says the compiler, correctly; so the connect lives in a script the trigger merely starts.
Lesson: a comment on a health number carries more weight than a comment on the weather, and every part of the pipeline has to be built as though it knew that, which mostly means erring strict and never letting the presentation improvise. When a reading fails to arrive, say why, with a number, because "the Bluetooth is flaky" is not something a person can act on at seven in the morning with a cuff on their arm. And when a device says the same thing every time, do not build the trigger on being told; ask whether the time of the last thing it said has moved.