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.