Specific, reproducible ways a home automation system misleads you. Each of these cost real debugging time. They are catalogued here so they can cost you nothing, which is not how it will go, but one likes to be civilised about these things.
What follows is a field guide, in the naturalist's sense: each trap is named, its habitat described, and its bite documented from experience. None of them is exotic. They live in ordinary configuration files, and they wait.
Configuration that validates but does nothing
On some devices a scheduled job or script is accepted without complaint and only fails at run time, a syntax variant the parser tolerates on creation and rejects on execution. A successful "add" proves nothing.
The self-feeding log loop
An automation triggered by error events, which itself logs errors, is a snake eating its own tail at several hundred thousand events an hour. It will fill a disk overnight, and the disk filling produces more errors, which feed the loop faster.
Beyond the obvious "exclude your own errors", the subtler lesson: a template that renders synchronously inside an event dispatch can re-enter the rendering machinery and throw. The thrown error is logged, the log event re-triggers the automation, and the loop is complete without any of your logic ever running.
An earlier version of this page then offered a fix: move the filtering out of the condition and into the action, where it runs in its own task. That was written after the first incident, verified to the satisfaction of everyone involved, and wrong. Three days after the fix, the identical loop returned, with the traceback running straight through the filter in its new location.
The reasoning about task isolation was plausible, which is all it ever was. (Two independent reasons it could never have held: the observed traceback shows the re-entrancy error returning straight through the relocated filter, so the isolation was not even achieved; and even if it had been, a template error in the action is itself logged at error level, which the trigger then re-catches. Either one alone closes the loop.)
The fix that has held is not a better place for the template. It is no template at all: the filter is now an external script that receives three strings and makes every decision with plain string comparisons, in a separate process, where the rendering machinery does not exist. It cannot re-enter what it never enters. Three details matter more than they look:
- The script always exits 0. A failing shell command gets logged as an error, which would feed the same loop through the back door.
- A rate cap: above a fixed number of lines a minute it writes one "suppressed" notice and stops. Even a fault nobody predicted cannot fill the disk again.
- Its own name stays in the ignore list, so this machinery's failures can never become evidence for itself.
The general shape: when a mechanism keeps finding new ways to feed itself, stop guarding the paths and remove the food.
Containers that are "started" with nothing running inside
A supervisor reports an add-on as running because its init process is alive. The service inside can have been dead for hours. It is a shop with the lights on, the door unlocked, and nobody behind the counter. Health checks, restart policies and the supervisor's own watchdog all report green.
Detect it from outside: probe the port, or check that the process you care about, not the init shim, is present. A tell-tale sign is an add-on that is "started" with an empty log. (Listing processes inside another container needs container-runtime access, which is itself a privilege worth noticing you are granting to a health check.)
Polled sensors that freeze at their last value
Command-line and template sensors can stop updating while retaining their last state. They do not go unavailable; they simply stop changing. If that sensor is a watchdog, it freezes at "healthy" and stays there.
Worse, the obvious remedy, asking the platform to update the entity, can return success without doing anything. Only reloading the whole integration recovers them.
The nuclear reload
A global "reload all" is tempting and occasionally catastrophic: it can unregister legacy notification targets for minutes, during which every automation that tries to notify fails, including the ones telling you something is wrong. Reload the specific domain you changed.
Encoding mismatches that fail completely silently
A webhook receiving a JSON body sent with a form content-type: the entire document arrives as a single field name with an empty value. Every lookup returns nothing and the request looks empty rather than malformed.
Worse, genuinely malformed JSON may cause the trigger not to fire at all, no log, no trace, nothing to distinguish it from a request that was never sent. If you control the sender, prefer form-encoded fields for free text: quotes and newlines in a message body break JSON, and they break it invisibly.
One template error in a variables block kills the whole run
If you compute a convenience variable at the top of an automation and it throws, because a trigger of a different shape left something undefined, the run aborts before anything executes, and it can do so without an obvious error. Read each field defensively at the point of use rather than building an intermediate structure from trigger data.
Editing a config file by pattern match
Two mistakes with the same root cause, both committed here, both more than once. First: indentation inferred from a printed excerpt that had spaces added by the very tool doing the printing, the patch then targets a structure that does not exist. Second: a replacement anchored on a common pattern, applied to the first match in the file, which lands in an entirely different automation that happened to share those lines.
Aggregations that hide a broken member
A mean across sensors will carry a badly-calibrated one for years. A median will not. For any aggregate over independent devices, prefer the median and expose the spread, a widening spread is itself an early warning that one member is drifting.
The watchdog that counted correctly and could not complain
A capture-health sensor judged delivery paths by name. The paths were later renamed; the sensor kept counting the old name, so it reported a healthy path as missing for days, a watchdog watching a ghost. The second fault was the better one: its degraded state was the bare path name, while the alert automation triggered on the substring "stale", which no longer appeared in any state the sensor could produce. It counted faithfully and said nothing, the exact shape of a monitor that fails "healthy", achieved through vocabulary rather than freezing.
Two habits. When you rename anything, grep the watchers for the old name; they are the component nobody remembers to migrate. And test the alarm's trigger against every state its sensor can emit, not just the one you expect, because a condition and a vocabulary drift apart the moment they live in different files.
Cache-busting you forgot to bust
If your dashboard embeds a locally-served page with a version parameter in the URL, editing the file changes nothing visible until you increment that parameter. You will verify server-side that your change is present, and still not see it.
We keep adding to this list, which is either a sign of diligence or evidence that the traps are winning. We have decided it is diligence.
Reloading a template is an event
Reloading template entities recreates them; for a moment each one is unavailable
and then back to its value. A state trigger written as to: 'on' fires on that return,
because unavailable → on is a transition to "on".
Every configuration edit that
touched templates therefore sent the household a "draught active" notice, raised a projector
screen that was already up, and would have switched off holiday mode had one been running. The
fix is dull and total: every state trigger on a template entity carries from: the
opposite state. Eighteen were hardened in one pass; the draught notice also gained a two-hour
cooldown measured from its last delivery, not its last attempt.
A daylight edge that never comes back
The couch light's "room got bright, switch off" trigger was a numeric-state rule: indoor illuminance above 30 lux for two minutes. It is an edge trigger, and in daylight the sensor reads a thousand lux all day, so it had crossed 30 at dawn and could not cross it again.
When the "got dark" path (a level rule on a different sensor) switched the light on at half past eight in the morning because a floor lamp had tricked the darkness estimate, nothing could switch it off, and it burned until somebody noticed at lunch. On and off now read the same level sensor, and the earlier lesson on edges gained a worked example.
Orphans that are not
A housekeeping add-on reported seven orphaned statistics every morning: series with no entity. The entities existed; they belong to a network controller that is deliberately enabled for four hours a night and disabled by day, so at report time they are absent from the state machine.
Ignoring the report did not survive a restart, because the add-on recreates it on every start. The durable fix was at the source: those seven entities record four hours of access-point CPU per night that nobody will ever chart, so they were disabled and their statistics deleted, and the report has nothing left to find.
One owner per Bluetooth address
The house's Bluetooth layer gives every device a single source and drops the other scanners' reports before any listener sees them; a challenger must be sixteen decibels louder to take over, and a repeated advertisement with unchanged contents is not passed on at all. A device that says the same thing every time, a blood pressure cuff, a beacon, triggers a listener once per appearance, or never. The cuff page has the whole night.
A switch for a lamp on the power rail
A microcontroller board by the bed has a red light that is annoying at night. We built a switch for it. The switch compiled, flashed, appeared in the house, toggled, and changed nothing, because the red light is wired to the supply rail through a resistor and has never met a pin the firmware can reach. The only working configuration is tape.
Two memories written at different times
The watering system remembers the soil moisture before a run, and separately the time of the last successful run, and a checker reads both every ten minutes to decide whether water actually arrived. The run wrote the first memory at the start and the second at the end, two and a half minutes apart. The checker fired in between, read a fresh "before" value against a three-day-old "last run", concluded that seventy hours had passed with no effect, and locked the pump. The pump, meanwhile, was running perfectly well.
Any check that reads two values written by different steps is a check with a race in it. The fix is to write both at the same moment, at the end, and to have the checker refuse to judge while the thing it is judging is still in progress.
Locking the owner out, for safety
A forgotten laptop in another country kept trying an old password every few minutes. The platform did the right thing and banned the address after five failures. The address was the household's own network, from which every phone, every notification and every remote fix is made. The ban cannot be lifted without a restart, and the restart has to be done from the very network that is banned. We got in through a side door we had wisely left open, cleared the ban, restarted, and discovered a second ban that had appeared while we were reading the first one. Read the ban list immediately before the restart, not ten minutes before. The system did exactly what it was told. It usually does.