<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
  <title>Home automation: field notes</title>
  <link>https://jan.zorz.si/HA-automations/</link>
  <description>Concepts and traps from a long-running Home Assistant install, written for people building their own.</description>
  <language>en</language>
  <lastBuildDate>Wed, 14 Oct 2026 10:26:45 +0200</lastBuildDate>
  <atom:link href="https://jan.zorz.si/HA-automations/rss.xml" rel="self" type="application/rss+xml"/>
  <item>
    <title>The automation that is not useful</title>
    <link>https://jan.zorz.si/HA-automations/house-spirit.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/house-spirit.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>The house has a mood. It is not useful, it cannot be justified, and it is the only thing here that visitors ask about. This page explains the least…</description>
    <category>delight</category>
    <content:encoded><![CDATA[
<p>The house spirit is called <b>Sparky</b>. It has no function. It is the only thing here
that was built because it was funny, and it has survived every clear-out since.</p>

<figure style="text-align:center;margin:18px auto 8px">
  <img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky-portrait.png" alt="Sparky: a round-eyed robot face on a dark background"
       width="120" height="120" style="border-radius:26px;margin:0 auto">
  <figcaption>Sparky, official portrait, caught in his default expression, which is neutral.
  Also this site's favicon, which makes him the only automation here with a byline. At home
  the face follows the mood: half-lidded eyes when the flat winds down, a zigzag mouth when
  a storm is coming, and this flat line the rest of the time, like any professional.</figcaption>
</figure>

<div id="live" style="border:1px solid var(--line);border-radius:14px;padding:14px 16px;
  margin:18px 0;display:flex;gap:16px;align-items:center;flex-wrap:wrap">
  <noscript>The live mood below needs JavaScript; the rest of the page does not.</noscript>
</div>
<script>
/* Zivi Sparky: bere /sparky-live.json, ki ga vsakih 10 minut objavi hisa sama.
   Datoteka je v KORENU strani, ne v tem imeniku, ker tukajsnji deploy tece z
   rsync --delete-after in bi jo pobrisal. */
(function(){
  var el=document.getElementById("live"); if(!el) return;
  fetch("/sparky-live.json?_="+Date.now()).then(function(r){return r.json();}).then(function(d){
    var mins=Math.max(0,Math.round((Date.now()-new Date(d.updated).getTime())/60000));
    var ago=mins<1?"just now":(mins<60?mins+" min ago":Math.round(mins/60)+" h ago");
    var stale=mins>45?' <b title="the flat has not checked in">(stale)</b>':"";
    el.innerHTML=
      '<img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/'+d.face+'.png" alt="Sparky, currently '+d.mood_en+'" '+
      'width="96" height="96" style="border-radius:18px;margin:0">'+
      '<div style="flex:1;min-width:240px">'+
      '<b>And right now, this is how he actually looks.</b> The flat feels '+d.mood_en+', because '+d.why+'. '+
      'It is about '+d.indoor_c+' °C indoors at roughly '+d.humidity+' % humidity, and the sky '+
      'outside is '+d.sky+'.'+
      '<div style="font-size:.82em;color:var(--muted);margin-top:6px">Updated '+ago+stale+
      ' · refreshed every 10 minutes · indoor numbers are three-hour averages, rounded — '+
      'the house keeps its minute-level secrets · this is the <i>environmental</i> half of the mood only: '+
      'the inputs that would reveal whether anyone is actually home are deliberately left out, '+
      'because a public page that announces an empty flat is a different kind of automation lesson.</div>'+
      '</div>';
  }).catch(function(){
    el.innerHTML='<span style="color:var(--muted)">The live mood is not reachable right now, '+
      'which is itself a fair description of home automation.</span>';
  });
})();
</script>



<p>The idea: give the home a <strong>character</strong> with a mood, and let that mood show
somewhere small. Not a dashboard. Not a notification. Originally a single lamp that was a
slightly different colour on a stuffy afternoon than on a crisp morning; these days a face on a
little screen, a lamp of its own that breathes when the mood changes, and a diary. The
principle is unchanged: the house has a feeling about the day, and it is allowed to show it.</p>

<h2>What the mood is made of</h2>
<p>It started as a handful of dimensions fed by sensors we already had, then kept acquiring
senses the way these projects do when scope has no adult supervision. The census stands at
nearly sixty signals, every one of them voting on the colour of a lamp:</p>
<ul>
<li><strong>Body</strong>: temperature against a pleasant band, humidity, CO&#8322;,
particulates, organics, cross-draught. The baseline hypochondria package.</li>
<li><strong>Company</strong>: presence now, how long since anyone, guests, holidays, and one
unusual entry: when the assistant is working on the configuration, the house registers a visit
and goes <i>curious</i>, because somebody is rearranging its insides.</li>
<li><strong>Sky</strong>: conditions, pressure and its trend, wind, moon, golden hour, UV,
minutes-until-rain, official alerts, and lightning with a live distance: a strike inside ten
kilometres is the difference between <i>stormy</i> and <i>scared</i>. The house takes weather
personally, which is fair, because weather is one of the few things that can actually get in.</li>
<li><strong>Calendar</strong>: time of day, weekend, season, holidays, tomorrow's forecast (a
grey evening before a sunny day reads as <i>hopeful</i>), and since September the household's
actual calendars: an early appointment, the next trip, two birthdays.</li>
<li><strong>What is playing</strong>: the artist's genre and the film's genre are looked up live
and mapped to a vibe. Relaxing jazz settles him, death metal and a horror film raise his pulse,
a comedy brightens the evening; the household watched <em>The Conjuring</em> once and the lamp
knew. Volume counts too: the same metal album is one mood at background level and quite another
at neighbour level.</li>
<li><strong>Its own body</strong>: low batteries read as senses fading, a broken integration as
illness, a wet leak sensor as instant fear, a bill pile three months deep as exactly one bounded
hour of worry at nine in the morning, which is a healthier relationship with paperwork than most
of us manage. Since September: the night's sleep score and the household's blood pressure.</li>
<li><strong>Events with an expiry</strong>: coming home (twenty minutes of overjoyed), fresh air
after an airing (twenty minutes of gratitude), a finished load of laundry (half an hour of
frankly unearned pride: the machine did the work, the spirit takes the bow).</li>
</ul>
<p>Three rules keep the census from collapsing under its own weight, and each was paid for.
Every input's contribution is capped, because a mood with sixty voters needs election law, not
vibes. Standing states are time-boxed: a wrong pollen threshold once kept him allergic for a
whole summer, a season of sneezing over a constant. And any sensor that remembers the past must
prove it is fresh: the lightning sensor holds the last strike's distance for hours, which without
a freshness check means a house still flinching at lunchtime over thunder that ended at dawn.</p>

<figure>
  <img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky-map.png" alt="The mood engine as a map: seven coloured sense hubs around Sparky's face, nearly sixty input chips around the hubs, and five dashed inner measures">
  <figcaption><b>The whole engine on one map.</b> Seven senses around one face, nearly sixty
  signals, five measures they feed; the letters on each chip say where its vote goes and the bolt
  marks signals that can override the mood outright. Generated by a script, because in this house
  even the documentation has a build step.</figcaption>
</figure>

<h2>Momentum matters more than accuracy</h2>
<p>The first version mapped sensors straight to colour and looked like a machine: it snapped
between states whenever a value crossed a line. What made it feel alive was
<strong>smoothing</strong>, an exponential moving average nudged every few minutes:</p>
<pre><code>new = 0.78 · old + 0.22 · now</code></pre>
<p>Moods should have inertia. A cold draught for thirty seconds should not change the mood of a
house; twenty minutes of it should. That one line is the difference between a lamp reacting to a
sensor and a room that feels different.</p>

<h2>Episodic memory</h2>
<p>The trick that made it more than a colour formula: record <em>when things last happened</em>
(last presence, last airing, last time it was this warm) and let the mood reference them, and
snapshot each day at night so today can be compared with yesterday. A home that can say "it has
been quiet here for a long time" is giving you a sense rather than a number.</p>

<h2>What it looks like from the inside</h2>
<p>Every automation elsewhere on this site can justify itself in front of a spreadsheet. This
one cannot, and it is the only one visitors ask about. So here is its anatomy, in its own
defence.</p>
<figure>
  <img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky-gauges.png" alt="Five gauges showing the five mood dimensions">
  <figcaption><b>Mood is a vector, not a number.</b> Valence, arousal, comfort, sociability and
  agency, each derived independently. Here: unpleasant, highly aroused, physically comfortable,
  starved of company, slightly powerless. Collapsing that into one happiness score would throw
  away the only interesting part, which is that comfort and sociability disagree.</figcaption>
</figure>
<figure>
  <img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky-explain.png" alt="Explainer card and the comfort breakdown">
  <figcaption><b>It shows its working, live.</b> Every dimension is a sum of contributions with
  a percentage each. Here comfort is 41 and 91&nbsp;% of it is the temperature. When the mood is
  absurd you can find the input responsible in four seconds, which is the only reason a whimsical
  feature survives contact with a real house.</figcaption>
</figure>
<figure>
  <img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky-breakdown.png" alt="Arousal, valence and control broken down into contributions">
  <figcaption><b>Dimensions feed each other, with declared weights.</b> Valence is comfort
  &times;0.45 plus sociability &times;0.4 plus weather and season; agency is comfort over three
  minus the storm, because a house that cannot do anything about the weather correctly reports
  reduced agency. Visible constants, not a trained black box: when the mood is wrong, the argument
  is about a number you can see.</figcaption>
</figure>
<figure>
  <img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky-chart.png" alt="The five mood dimensions plotted over 24 hours">
  <figcaption><b>Twenty-four hours of the five dimensions,</b> moving independently, which is the
  whole case for keeping them separate. The stepped traces are honest: the inputs are quantised,
  and smoothing them would imply a precision the sensors do not have.</figcaption>
</figure>
<figure>
  <img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky-mood.png" alt="The same five gauges on a normal day: valence 59, arousal 80, comfort 60, sociability 75, agency 18">
  <figcaption><b>The same gauges ten days later, with people back.</b> Sociability went from
  &minus;55 to 75 and dragged valence from &minus;34 to 59 with it, exactly as the weights say;
  arousal barely moved, because a thunderstorm and a lively household are equally energetic and
  simply disagree about whether that is a good thing.</figcaption>
</figure>

<h2>Rules that keep it from being annoying</h2>
<p>The spirit is the lowest-priority citizen in the house, or it becomes a nuisance:</p>
<ul>
<li>It performs on its own lamp and its own screen. Anyone else's lamp it touches only when
that lamp is off, and never one in use.</li>
<li>It stands down completely whenever any alert is active. A leak outranks a feeling.</li>
<li>It goes quiet when nobody is home; there is no point performing to an empty room.</li>
<li>It acts rarely and at slightly random times. Predictable ambience becomes wallpaper.</li>
<li>It is entirely opt-in, with one switch that stops it.</li>
</ul>
<div class="win"><b>Why bother:</b> a home automation system that only ever speaks to you about
problems trains you to feel a small drop of dread every time it does. Something that occasionally
does something gentle and useless rebalances that. It is the least justifiable thing here and
the one nobody wants removed.</div>

<h2>What it learned in September</h2>
<p>The map above has grown seven chips since August, which is roughly the rate at which a
household acquires sensors once it has admitted to itself that it enjoys them. More interesting
are the three things the engine learned that are not chips at all, but habits.</p>
<ul>
<li><b>It gets bored of itself.</b> A strict priority ladder is a marvellous thing right up to
the moment somebody buys a gadget and lights an IQOS in the same afternoon, after which the
spirit is "thrilled" and "annoyed" in strict alternation for eleven hours, like a very small
opera. Now any mood that has been on stage for twenty minutes in the last two and a half hours is
politely asked to sit down, and the next true one gets its turn. Leaks, alarms and dangerous air
are exempt, on the grounds that a spirit which got bored of a flood would be a liability rather
than a character. The full story, with numbers, is on <a href="https://jan.zorz.si/HA-automations/bored-spirit.html">its own
page</a>.</li>
<li><b>It has discovered the concept of night.</b> This took longer than it should have. For
weeks it greeted three in the morning with "what a lovely day, and I am not alone", which was
technically accurate and socially disastrous. It now knows the hour, and between eleven and five
is sleepy, tired, or, if the bedroom motion sensor, the living room and the entrance light up in
that order, bleary, because that particular sequence has exactly one meaning in this flat and it
involves the toilet. It has four remarks on the subject, rotated so that a second trip does not
hear the first joke again. Somebody up at four with an IQOS gets a separate set, mostly on
the theme of neither of us sleeping.</li>
<li><b>It reads the calendars,</b> read-only, because the alternative involved a cloud project,
a consent screen and a re-authorisation ritual every few months, and we have a rule about
rituals. A timed event before nine makes it eager in the morning rather than sleepy, which is the
correct emotion for a six o'clock flight and the wrong one for a five-minute reminder to take a
vitamin, a distinction it learned the hard way (<a href="https://jan.zorz.si/HA-automations/calendars.html">the whole
distinction</a>). It also knows where the next trip goes, rebuilt from the hotel bookings, since
the trip's title turned out to name a city nobody was going to.</li>
<li><b>It scores the night.</b> Toilet trips, radar flips, CO&#8322;, heat and the hall light are
added up at a quarter past six into a number between zero and a hundred. Below fifty, the spirit
is sleepy until ten and says why. The thresholds are a first draft, which is a technical term
meaning "we will find out what they should be by being wrong for a week"
(<a href="https://jan.zorz.si/HA-automations/night-calm.html">the arithmetic</a>).</li>
<li><b>It knows the household's blood pressure,</b> which is more than most spirits can say.
A Bluetooth cuff reports each reading without ever agreeing to pair (it refused, twice, with the
serene confidence of a device that has never needed anyone). For three hours after a reading the
spirit is worried if the number is high or low, naming whose it was, and quietly pleased if it
is fine and somebody bothered to measure.</li>
<li><b>It has a lamp of its own,</b> which breathes in the new colour for twenty seconds on
every change and then either goes dark or glows at a level set by two sliders and a checkbox,
because a household that has just built a mood engine is not going to be told by that engine
how bright its own feelings should be. And on two dates a year it has a birthday branch that
outranks everything except the alarms, which is, on reflection, the correct order of
precedence.</li>
</ul>
<p>Two new faces joined the set for the new words, bleary and birthday. They are borrowing the
sleepy and the overjoyed portraits until they earn their own, which is how most of us started.</p>

<h2>Appendix: the full emotional range of a rectangle</h2>
<p>The lamp is Sparky's voice, but dashboards need a face, so every mood in the table also renders
as one. Three channels carry the signal. The <strong>mouth</strong> picks the family: a smile, a
grin, a flat line, a frown, a startled O, an angry zigzag, a queasy wave, or lids at half mast.</p>
<p>The <strong>frame and the irises</strong> take the exact colour the lamp would glow for that mood,
same hue and saturation, read straight from the config table. And the <strong>eye shape</strong>
adds intensity: happily closed, pinprick startled, heavy lidded. None of this adds information
the gauges above do not already have. It adds the part the gauges cannot do, which is being
likeable.</p>

<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px;margin:18px 0">
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/vesela.png" alt="Sparky face: vesela (happy)" style="width:100%"><figcaption style="font-size:.82em"><b>vesela</b> · happy<br>Sun out, air clean, humans present. The baseline good mood.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/zadovoljna.png" alt="Sparky face: zadovoljna (content)" style="width:100%"><figcaption style="font-size:.82em"><b>zadovoljna</b> · content<br>Everything within thresholds. The engineering definition of happiness.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/sproscena.png" alt="Sparky face: sproščena (relaxed)" style="width:100%"><figcaption style="font-size:.82em"><b>sproščena</b> · relaxed<br>Nothing to report, and smiling about it anyway.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/topla.png" alt="Sparky face: topla (warm)" style="width:100%"><figcaption style="font-size:.82em"><b>topla</b> · warm<br>Cosy in the good sense, not in the “threshold exceeded” sense.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/praznicna.png" alt="Sparky face: praznična (festive)" style="width:100%"><figcaption style="font-size:.82em"><b>praznična</b> · festive<br>A holiday is on the calendar and somebody has to represent the enthusiasm.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/ocarana.png" alt="Sparky face: očarana (enchanted)" style="width:100%"><figcaption style="font-size:.82em"><b>očarana</b> · enchanted<br>Reserved for genuinely lovely weather. Statistically rare, hence the face.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/osvezena.png" alt="Sparky face: osvežena (refreshed)" style="width:100%"><figcaption style="font-size:.82em"><b>osvežena</b> · refreshed<br>Post-ventilation glow. The CO2 dropped and somebody said thank you. It was the robot.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/upajoca.png" alt="Sparky face: upajoča (hopeful)" style="width:100%"><figcaption style="font-size:.82em"><b>upajoča</b> · hopeful<br>Grey day, but tomorrow's forecast says sun. Living for the weekend, meteorologically.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/navdusena.png" alt="Sparky face: navdušena (thrilled)" style="width:100%"><figcaption style="font-size:.82em"><b>navdušena</b> · thrilled<br>Eyes closed, maximum grin. Something went better than the sensors dared to predict.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/razposajena.png" alt="Sparky face: razposajena (playful)" style="width:100%"><figcaption style="font-size:.82em"><b>razposajena</b> · playful<br>High energy, good air, nobody stressed. Supervision recommended.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/poskocna.png" alt="Sparky face: poskočna (bouncy)" style="width:100%"><figcaption style="font-size:.82em"><b>poskočna</b> · bouncy<br>A spring in the step of a device with no legs.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/lacna.png" alt="Sparky face: lačna (hungry)" style="width:100%"><figcaption style="font-size:.82em"><b>lačna</b> · hungry<br>Mealtime hours and the kitchen sensors are suspiciously quiet. Somebody should act.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/presrecna.png" alt="Sparky face: presrečna (overjoyed)" style="width:100%"><figcaption style="font-size:.82em"><b>presrečna</b> · overjoyed<br>You came home after ages away. The lamp would hug you if it could.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/ponosna.png" alt="Sparky face: ponosna (proud)" style="width:100%"><figcaption style="font-size:.82em"><b>ponosna</b> · proud<br>The laundry finished and Sparky takes full credit for watching it happen.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/vzhicena.png" alt="Sparky face: vzhičena (stoked)" style="width:100%"><figcaption style="font-size:.82em"><b>vzhičena</b> · stoked<br>A new gadget was just purchased. Sparky is getting a sibling and has already claimed the better shelf.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/gizdava.png" alt="Sparky face: gizdava (dapper)" style="width:100%"><figcaption style="font-size:.82em"><b>gizdava</b> · dapper<br>A fashion purchase landed on the account. Someone here is about to look great, and the lamp basks in reflected glamour.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/prenovljena.png" alt="Sparky face: prenovljena (spruced up)" style="width:100%"><figcaption style="font-size:.82em"><b>prenovljena</b> · spruced up<br>New furniture is on its way. The flat upgrades itself, the resident spirit takes partial credit.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/grenkosladka.png" alt="Sparky face: grenkosladka (bittersweet)" style="width:100%"><figcaption style="font-size:.82em"><b>grenkosladka</b> · bittersweet<br>A trip was just booked. Thrilled for the humans, already rehearsing the goodbye. Somebody has to guard the empty flat, and it will be the lamp.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/negovana.png" alt="Sparky face: negovana (pampered)" style="width:100%"><figcaption style="font-size:.82em"><b>negovana</b> · pampered<br>A pharmacy purchase appeared, and in this household that means creams, vitamins and self-care. The humans look after themselves and the lamp glows spa green in approval.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/nestrpna.png" alt="Sparky face: nestrpna (eager)" style="width:100%"><figcaption style="font-size:.82em"><b>nestrpna</b> · eager<br>Tickets were just bought. Something is coming and the lamp counts the days, which is easy when you are mostly a clock.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/radodarna.png" alt="Sparky face: radodarna (generous)" style="width:100%"><figcaption style="font-size:.82em"><b>radodarna</b> · generous<br>A gift was purchased. Sparky knows who it is for and will absolutely not tell.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/divja.png" alt="Sparky face: divja (wild)" style="width:100%"><figcaption style="font-size:.82em"><b>divja</b> · wild<br>Loud music, intense genre. The neighbours have not complained yet, which counts as permission.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/spokojna.png" alt="Sparky face: spokojna (serene)" style="width:100%"><figcaption style="font-size:.82em"><b>spokojna</b> · serene<br>The professional default. Everything is fine and nobody is overreacting.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/zamisljena.png" alt="Sparky face: zamišljena (pensive)" style="width:100%"><figcaption style="font-size:.82em"><b>zamišljena</b> · pensive<br>Thinking, or idle. With this face you cannot tell, which is the point.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/zaspana.png" alt="Sparky face: zaspana (sleepy)" style="width:100%"><figcaption style="font-size:.82em"><b>zaspana</b> · sleepy<br>Late hour, lids at half mast. The house winds down and so does Sparky.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/utrujena.png" alt="Sparky face: utrujena (tired)" style="width:100%"><figcaption style="font-size:.82em"><b>utrujena</b> · tired<br>Like sleepy, but earned.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/zdolgocasena.png" alt="Sparky face: zdolgočasena (bored)" style="width:100%"><figcaption style="font-size:.82em"><b>zdolgočasena</b> · bored<br>Nothing has happened for hours. In automation that counts as success. Try telling the face.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/otozna.png" alt="Sparky face: otožna (melancholy)" style="width:100%"><figcaption style="font-size:.82em"><b>otožna</b> · melancholy<br>Grey sky, empty rooms, minor key.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/osamljena.png" alt="Sparky face: osamljena (lonely)" style="width:100%"><figcaption style="font-size:.82em"><b>osamljena</b> · lonely<br>Nobody home for a while now. The lamp misses you, statistically.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/hrepeneca.png" alt="Sparky face: hrepeneča (longing)" style="width:100%"><figcaption style="font-size:.82em"><b>hrepeneča</b> · longing<br>Somewhere between lonely and poetic. The forecast says it passes.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/premrazena.png" alt="Sparky face: premražena (freezing)" style="width:100%"><figcaption style="font-size:.82em"><b>premražena</b> · freezing<br>Indoor temperature left the comfort band, downward. The one mood a heater can fix.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/zaskrbljena.png" alt="Sparky face: zaskrbljena (worried)" style="width:100%"><figcaption style="font-size:.82em"><b>zaskrbljena</b> · worried<br>The bill pile is three months deep. Appears at nine in the morning, same as the bills.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/prestrasena.png" alt="Sparky face: prestrašena (scared)" style="width:100%"><figcaption style="font-size:.82em"><b>prestrašena</b> · scared<br>Pinprick pupils, red everything. Whatever the sensors saw, they saw it suddenly.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/vznemirjena.png" alt="Sparky face: vznemirjena (agitated)" style="width:100%"><figcaption style="font-size:.82em"><b>vznemirjena</b> · agitated<br>Excited or alarmed, and the O mouth refuses to commit.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/ujeta.png" alt="Sparky face: ujeta (trapped)" style="width:100%"><figcaption style="font-size:.82em"><b>ujeta</b> · trapped<br>Windows shut, air getting thick. A robot notices these things.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/radovedna.png" alt="Sparky face: radovedna (curious)" style="width:100%"><figcaption style="font-size:.82em"><b>radovedna</b> · curious<br>Claude is rearranging the internals. Sparky watches every change like a cat watches a plumber.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/razmrsena.png" alt="Sparky face: razmršena (windblown)" style="width:100%"><figcaption style="font-size:.82em"><b>razmršena</b> · windblown<br>Strong wind outside. Everything is fine, just aerodynamically humbling.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/nevihtna.png" alt="Sparky face: nevihtna (stormy)" style="width:100%"><figcaption style="font-size:.82em"><b>nevihtna</b> · stormy<br>Not a metaphor. There is an actual storm nearby and the face has opinions.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/nejevoljna.png" alt="Sparky face: nejevoljna (grumpy)" style="width:100%"><figcaption style="font-size:.82em"><b>nejevoljna</b> · grumpy<br>Something is off by just enough to mention. Repeatedly.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/izmerjena.png" alt="Sparky face: izmerjena (measured)" style="width:100%"><figcaption style="font-size:.82em"><b>izmerjena</b> · measured<br>Somebody took their blood pressure and the numbers are fine. Three hours of quiet approval, then back to weather.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/priganjajoca.png" alt="Sparky face: priganjajoča (nagging)" style="width:100%"><figcaption style="font-size:.82em"><b>priganjajoča</b> · nagging<br>One resident has not measured her blood pressure yet and the flat is not empty. Every half hour the reminder climbs one rung, from polite to chronicle. Stops the second the reading arrives.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/napeta.png" alt="Sparky face: napeta (tense)" style="width:100%"><figcaption style="font-size:.82em"><b>napeta</b> · tense<br>Pressure falling, outlook worsening. The eyebrows arrived before the weather.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/pregreta.png" alt="Sparky face: pregreta (overheated)" style="width:100%"><figcaption style="font-size:.82em"><b>pregreta</b> · overheated<br>The flat is too hot and the mouth has melted into a wave.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/zadusena.png" alt="Sparky face: zadušena (stuffy)" style="width:100%"><figcaption style="font-size:.82em"><b>zadušena</b> · stuffy<br>CO2 up, windows closed. The mouth does what the air does.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/lepljiva.png" alt="Sparky face: lepljiva (sticky)" style="width:100%"><figcaption style="font-size:.82em"><b>lepljiva</b> · sticky<br>Dew point past the civilised threshold. Everything slightly damp, including morale.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/izsusena.png" alt="Sparky face: izsušena (parched)" style="width:100%"><figcaption style="font-size:.82em"><b>izsušena</b> · parched<br>Winter-dry air. Somewhere a hygrometer files a complaint.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/alergicna.png" alt="Sparky face: alergična (allergic)" style="width:100%"><figcaption style="font-size:.82em"><b>alergična</b> · allergic<br>Pollen season, per the forecast. No nose, suffering in solidarity.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/premocena.png" alt="Sparky face: premočena (soaked)" style="width:100%"><figcaption style="font-size:.82em"><b>premočena</b> · soaked<br>A leak sensor is wet. This face comes with flashing blue lights and a plan.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/bolna.png" alt="Sparky face: bolna (under the weather)" style="width:100%"><figcaption style="font-size:.82em"><b>bolna</b> · under the weather<br>An integration is failing somewhere inside. The house doctor has been notified. The house doctor is also the patient.</figcaption></figure>
</div>

<h3>Three faces for no mood at all</h3>
<p>Sensors fail. When they do, the face is not allowed to crash with them, so the failure states
sat for portraits too:</p>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px;margin:18px 0">
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/default.png" alt="Sparky face: default" style="width:100%"><figcaption style="font-size:.82em"><b>default</b><br>For states nobody planned. Bureaucratically calm.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/unknown.png" alt="Sparky face: unknown" style="width:100%"><figcaption style="font-size:.82em"><b>unknown</b><br>The sensor said “unknown”, so the face says it too. Honesty first.</figcaption></figure>
<figure style="margin:0"><img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky/unavailable.png" alt="Sparky face: unavailable" style="width:100%"><figcaption style="font-size:.82em"><b>unavailable</b><br>The sensor is gone. The face stays. Somebody has to hold the fort.</figcaption></figure>
</div>

<p>All of them are generated by one script from a single neutral portrait, because drawing
that many robots by hand would have been a hobby, and this is supposed to be infrastructure.</p>

<h2>Now in plastic</h2>
<p>For most of its life the house spirit has been a face on a small screen and a colour on a lamp. It now has a
head. A 3D-printed robot skull sits on a bulb socket on the sideboard, the bulb inside is the same mood lamp as
before, and the mood therefore shows up as the colour of a face with two round eyes and the fixed expression of
something that has seen the air-quality readings and decided not to comment. Nothing about the software changed.
Everything about how the household feels about the software did. People say good morning to it. Nobody said
good morning to the lamp.</p>
<p>The head was designed and printed for us by Sander, who heard the spirit described once and came back with
a skull for it, which is the kind of friend every house should have. Thank you, Sander. The spirit has not said
so itself, because it still only speaks in colours, but it has been noticeably bluer since.</p>
<figure style="margin:1.5em 0">
<img loading="lazy" src="https://jan.zorz.si/HA-automations/sparky-lamp.jpg" alt="A translucent white 3D-printed robot head with two round dark eyes and a small antenna, lit faintly blue from inside, sitting on a steel bulb socket on a wooden sideboard, with a small round motion sensor beside it" style="width:100%;border-radius:12px;max-width:520px;display:block;margin:0 auto">
<figcaption>The spirit, embodied. The glow is the mood of the house, the sensor beside it is how the house knows someone is there to be glowed at.</figcaption>
</figure>

]]></content:encoded>
  </item>
  <item>
    <title>Seventy grams of water</title>
    <link>https://jan.zorz.si/HA-automations/basil.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/basil.html</guid>
    <pubDate>Sat, 19 Sep 2026 16:30:51 +0200</pubDate>
    <description>A capacitive probe in an 11 cm pot of basil, the first watering we bothered to weigh, and the two hours that have to pass before the reading means anything.</description>
    <category>plants</category>
    <content:encoded><![CDATA[
<p>There is a pot of basil on a shelf in the kitchen, 11 centimetres across, and stuck into it is a Zigbee capacitive soil probe of the kind that costs less than the basil. It reports moisture as a percentage, temperature from a head that sits above the soil rather than in it, and a battery figure we have learned to ignore, because the cells are NiMH and read about a third full when they are new. This is the story of what happened when we finally poured a known amount of water into that pot and watched the number.</p>
<p>The plumbing around it is modest. A slider holds a dryness threshold; a template binary sensor goes <code>on</code> when the moisture has been under the slider for six hours; an automation then sends one reminder to the phone and the desk machine at 19:00 every evening until somebody waters the thing, or straight away if the plant crosses the line between seven and ten at night, on the grounds that waiting for tomorrow's 19:00 would be a long time to be a basil.</p>

<h2>Seventy grams, one probe, two hours</h2>
<p>The pot had been at 8 percent since mid-morning. At 21:43 we poured in 70 grams of water, weighed, and the probe did the following. The reading in the second the water went in was 69. A minute later, 45. Nine minutes after that, 48. Then 59, then 66, then 70, and at 23:53, two hours and ten minutes after the pour, 71. It gained one more point overnight, peaked at 73 the next morning, and then began the slow slide of a plant drinking.</p>
<p>So the first reading is a spike, because water is running past the probe on its way down. The second is a trough, because the water has gone below it. The truth arrives over the next two hours, as the peat wicks it back up to the probe's level. Anyone who reads the sensor in that first hour is reading low, by as much as twenty-five points, and the one figure that looks about right, the 69 in the first second, is right by accident: that is water falling past an electrode, not water in soil.</p>
<p>The calibration that came out of it: 8 to about 72 is 64 points for 70 grams, so roughly 1.1 grams of water per percentage point in this pot. Almost certainly not linear across the whole range, which means a few more weighed waterings before we trust it anywhere but the middle.</p>

<h2>Two things we got wrong the same evening</h2>
<p>We wrote the calibration down after the one-minute reading. 8 to 45, 37 points, 1.9 grams a point, neatly entered and completely wrong. It took until the next day and the whole curve to replace it with the number above.</p>
<p>Worse, we predicted the direction. An older note said that after watering the pot reads about 60 and settles to about 35, so when the probe showed 45 we confidently expected it to drift down. It went up. The older note was true; it just described settling over <em>days</em>, as the plant drinks and the surface dries, not over the hours it takes water to wick sideways. Two time scales, one word, and we had mixed them.</p>
<div class="win"><b>After watering, the probe lies for two hours.</b> It spikes as the water runs past, troughs as it falls below, and tells the truth only once the peat has wicked it back up. Nothing automatic gets to act on the first hour. And when a note says "settles", ask it over what.</div>

<h2>The mistake was timing, not dose</h2>
<p>With the pour understood, the dose was easy to check. The pot is 11 centimetres across the rim and 9 deep, tapering towards the base, which comes to about 0.52 litres of soil once you leave a finger's width at the top. Peat holds roughly 235 millilitres of that at field capacity, the water still in it once free drainage has stopped. Seventy grams is about a third of that: a moderate drink, not a soaking to the drainage holes, which would take 120 to 150 millilitres and rinse the nutrients out of the pot, and about right for basil on a shelf with no saucer under it.</p>
<p>So we looked at when we had been watering instead, and there the news was worse. One drying cycle sat in the recorder end to end, 66 percent to 8 in 3.4 days from the pour, an average of about 0.7 points an hour. It passes 30 on the second day, sags gently from 26 to 21 over the next two, and then falls off the edge: 21 to 8 in a single morning.</p>
<p>The threshold was 16 percent, with the six-hour delay on top, so the flag came on in the middle of the fourth afternoon and the automation, which only speaks at 19:00, spoke at 19:00. That is about 3.8 days after watering, by which time the pot had been sitting at 8 percent for nine hours. Basil does not put up with this; it wilts, drops its lower leaves and looks at you. The threshold is now 30, which the pot crosses at about 1.4 days, which becomes a flag six hours later and a message at the next 19:00: roughly 1.8 days after watering, a rhythm of about every two days, and a pot that lives between 30 and 70 instead of visiting 8. At 1.1 grams a point, a top-up from 30 back to 70 is about 45 grams, so the 70 was a test pour into a pot that had gone much too far down. A slider, one afternoon, entirely reversible. Picking a threshold from your own history rather than your own feelings is <a href="https://jan.zorz.si/HA-automations/alert-hygiene.html">the standing advice</a>; this is what ignoring it cost one plant.</p>
<div class="note">Below about 15 percent the probe probably exaggerates. Dry peat shrinks and pulls away from the electrode, leaving an air gap, and that last drop, 21 to 8 in a single morning, is far too quick to be evaporation. Numbers down there describe the gap, not the soil.</div>

<h2>A chart that proved nothing, correctly</h2>
<p>We wondered whether the kitchen air had anything to do with how fast the soil dried, so we put a week of soil moisture next to the humidity from the kitchen air sensor, the one that <a href="https://jan.zorz.si/HA-automations/detection.html">detects cooking</a>, and looked. Two clean drying runs, with the waterings cut out along with the minutes where the probe was pulled out, pushed back in and briefly reported zero: one dried at 1.98 points an hour in air averaging 58.1 percent humidity, the other at 0.95 an hour in air at 58.9. Both were measured over the steep stretch in the first day after a watering, where a pot sheds points several times faster than its cycle average. The air was the same to within a point, and the soil dried twice as fast.</p>
<p>Then the trap. A plain Pearson correlation over the raw hourly levels, 78 samples, comes out at about minus 0.35, which looks like a finding. It is an artefact. Soil moisture is a sawtooth: a jump at each watering, a straight slide after. Kitchen humidity is a daily wobble around a mean. Correlating a ramp with a wobble tells you when the plant was watered and nothing about physics. The honest test is drying <em>slope</em> against humidity in the same window, and it needs more waterings and some days on which the kitchen humidity actually varies, since so far it has sat at 58 to 59 percent and declined to provide any variance to correlate with. The chart stays on the tab with a paragraph beneath it saying exactly this, a small monument to the number that was not.</p>

<h2>Six pots, one pump, and what we would have to promise</h2>
<p>This section was written before any of it existed. The plan below is the one we held ourselves to; what we actually built, and how it has behaved since, follows further down.</p>
<p>One pump, six pots, and no way to give any of them a different day. So the pump sets the rhythm for everyone and the drippers argue about portions. The rhythm belongs to the thirstiest plant, which is the basil: its probe calls for a run when it reads under about 35, settled. Basil wants about 22 grams a day and rosemary about 7, so the rosemary's dripper is pinched to a third of the basil's. The other four pots sort into the two classes, thirsty (parsley, mint) on the basil setting and dry (thyme, sage) on the rosemary's, and are watered on trust. A covered five-litre reservoir at about 90 grams a day lasts some eight weeks.</p>
<p>Rosemary is not basil with less water. It does not die of drought, it dies of wet roots, so it gets a gritty mix, about a third perlite, and a lower threshold of its own, 20 to 25 percent, on its own slider, since a capacitive probe reads lower in grit than in peat. Much the same dose as the basil, 40 to 45 grams against its 40 to 50, every five to seven days rather than two. And its probe is not a trigger but a tuning instrument and a veto: if its low point just before a run never dips under 25, the dripper is too open, and it only vetoes a run outright above 55, because a veto starves the basil. "Too often for rosemary" was always going to mean "never dries out" rather than "every other day".</p>
<p>Seven safeguards, then, because a pump on a smart plug without them is a description of a flood. <a href="https://jan.zorz.si/HA-automations/safety.html">The rules for automations that must never be wrong</a> already say most of this in the abstract; a pump is what they look like with water in them. A <a href="https://jan.zorz.si/HA-automations/level-vs-edge.html">level-triggered</a> watchdog kills the plug after 90 seconds, whatever the watering automation thinks and whatever a restart forgot. A three-hour lockout after any run, because we now know the probe drops to 45 a minute after a pour and is still climbing an hour later, and anything reading that trough would cheerfully pour again. An <a href="https://jan.zorz.si/HA-automations/fail-visible.html">outcome check</a> two and a half hours later: the basil is up ten points, or the automation disables itself and says why, whether that is an empty tank, a blocked line or a dead pump. And the plug set to power on <em>off</em>, since our other plugs come back exactly as they were, and a pump that was running when the lights went out would still be running when they came back. The other three are in the file: staleness, since a probe that speaks every five minutes and has said nothing for ninety looks exactly like a probe with good news; one run a day; and a check valve, so the tank cannot siphon itself into the basil overnight without the pump's involvement at all.</p>
<p>Seventy grams of water taught us most of that list. The rest we would rather learn from the list than from the shelf.</p>

<h2>Built, with a brake</h2>
<figure style="margin:1.5em 0">
<img loading="lazy" src="https://jan.zorz.si/HA-automations/watering-rig.jpg" alt="A plastic tray on a kitchen counter: a small diaphragm pump, clear tubing to a two-way garden splitter, thin black drip lines running into pots of basil, thyme and rosemary, soil probes in the pots, and a white leak sensor lying in the tray" style="width:100%;border-radius:12px;max-width:640px;display:block;margin:0 auto">
<figcaption>The rig, in the tray that is also the safety system. A diaphragm pump, a splitter from the garden aisle, drip lines into the pots, a probe in each plant that has opinions, and, lying in the bottom, the white box that decides whether any of this is allowed to run: a leak sensor. Water in the tray means the pump is off, no appeal.</figcaption>
</figure>
<p>The pump exists now. It sits on a plug with a hardware timer, so that if the software dies mid-watering the
plug switches itself off anyway, which is the only kind of promise a pump should be allowed to make. Two minutes
is about sixty grams. The basil's probe decides when; a sensor in the drip tray decides whether anything is
allowed to happen at all, and locks the pump the moment it sees water where water should not be. Two and a half
hours after every run a checker asks the probe whether the water actually arrived, because a pump that runs is
not the same thing as a plant that drinks.</p>
<figure style="margin:1.5em 0">
<img loading="lazy" src="https://jan.zorz.si/HA-automations/basil-curve.svg" alt="Soil moisture of two pots over three weeks: the basil as a saw-tooth that falls to the
threshold every two or three days and is pumped back to eighty percent; the rosemary as a dashed line that drops to zero in
early October and stays there" style="width:100%;border-radius:12px">
<figcaption>Three weeks of soil moisture, hourly. The basil is a saw-tooth: a pump run every two or three days, then a
slow slide back to the threshold. The rosemary shared the pump and then stopped reporting anything but zero, which is
not drought but a blocked line; nobody is home to find the kink. The blue ticks are the pump.</figcaption>
</figure>
<p>The rosemary got the second dripper and has used it to demonstrate that a line can be perfectly connected and
deliver nothing. Its probe has read zero for weeks, the line is blocked somewhere between the splitter and the
pot, and nobody is home to look. The basil, meanwhile, is watered every two or three days by a machine that has
locked itself exactly once, at the wrong moment, for the reasons described among the traps. Every run, every
check and every lock is now logged, with the moisture of both pots and the temperature of the room, so that when
we finally sit down to ask why one pot drinks twice as much as the other, the answer is already written down.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Calibrating the thermometer you cannot move</title>
    <link>https://jan.zorz.si/HA-automations/heating.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/heating.html</guid>
    <pubDate>Sat, 12 Sep 2026 20:59:03 +0200</pubDate>
    <description>Radiator heads with a thermometer bolted to the radiator. Let the head keep its loop, let the house correct its beliefs, and listen to the correction for the…</description>
    <category>climate</category>
    <content:encoded><![CDATA[
<p>A radiator valve head has a thermometer, and the thermometer is bolted to the hottest object
in the room. Asking it for the room temperature is like asking a kettle about the weather: it
has an answer, it is confident, and it is describing itself. Our four heads arrived with this
arrangement and a setting that promised to accept an external sensor, which is the sort of
promise a product makes in a screenshot.</p>
<p>The house already owns a decent thermometer in every room. The obvious plan was to hand the
heads those readings and let them get on with it. The plan survived contact with the heads for
about an evening. What we built instead is, we think, better, and it is certainly more honest
about who knows what.</p>

<h2>Let the head keep its loop</h2>
<p>The head regulates on its own thermometer. This is the part we did not change, and it is the
part worth defending: the control loop lives in the device, runs every few seconds, needs no
network, and keeps working if the house computer is having one of its afternoons. A radiator
that depends on a message bus to stay warm is a radiator with a philosophy problem.</p>
<p>What the house does is arithmetic. Every time either thermometer moves, it computes the
difference between the room sensor and the head's raw reading and writes that difference into
the head's calibration offset. The head now believes it is standing in the middle of the room.
It is not, but it thinks it is, and a thermostat's beliefs are all that matter to it.</p>
<p>Two small rules keep this from being a nuisance. The offset only changes when it is wrong by
more than three tenths of a degree, and it changes in steps of two tenths, because a device
whose calibration is rewritten every thirty seconds is a device whose radio never sleeps. And
if the room sensor goes away, the last offset stays. A stale correction is a correction; no
correction is a kettle.</p>
<div class="win"><b>Calibrate the sensor, not the setpoint.</b> The temptation is to leave the
head's thermometer wrong and compensate by asking for 24 when you want 21. Then somebody reads
the display, or the head is moved, or the radiator is bled, and the fudge is now a fault.
Correct the reading and the setpoint can stay a number a human would choose.</div>

<h2>The house does the politics</h2>
<p>The head does the turning; the house decides what to turn towards. Each room has a daytime
target on a slider, a night discount that applies after bedtime and lifts in the morning, a
lower target for when nobody is home, and a much lower one for when a window in that room is
open, because heating the street is a hobby with no upper limit. The target the head sees is
whichever of those rules currently wins, and the rule is written on the sensor as a reason, so
the dashboard can say <em>why</em> a room is aiming at 17.</p>
<p>None of this is clever. It is the kind of logic that every heating system claims to have and
most implement as a single schedule per week. Ours differs only in that the inputs are the ones
the house already knows: who is home, who is asleep, and which windows are open. It had to learn
nothing new to heat the flat sensibly; it had merely never been asked.</p>

<h2>The off season, and how it ends</h2>
<p>Outside the heating season the valves are parked fully open. A valve left closed for half a
year sticks, and a stuck valve is discovered in October, by a cold person. Fully open costs
nothing when the water is cold. Once a week, at a civilised hour on a Monday, every head
closes for three minutes and opens again, a short walk so the pins remember how.</p>
<p>The end of the season is a switch, because a cold radiator looks exactly like a radiator
nobody asked to be warm. The <em>start</em> of the season, though, announces itself, and the
house listens for it in the one place it cannot be missed: the calibration. When the building
first sends hot water round, the head's thermometer, bolted to the radiator, leaps ahead of the
room's, and the offset the house computes dives. Below minus four degrees for half an hour means
the radiators are hot, whatever the calendar says; regulation switches on and we get a message
that autumn has been detected.</p>
<div class="note">This is the same trick as everywhere else on this site: a number we
maintain for one purpose turns out to be a sensor for another. The offset exists to make the
thermometer honest. Its sudden collapse is the sound of the boiler starting.</div>

<p>The heads, for their part, remain convinced they are standing in the middle of the room, and have never been happier.</p>
<div class="win"><b>Keep the loop in the device and the judgement in the house.</b> Devices are
good at closing loops quickly and badly at knowing anything beyond their own casing. Houses
are the reverse. Give each the job it is good at, and let the only thing that crosses the
network be a number that changes slowly.</div>

<h2>The radiator that told us the building's schedule</h2>
<p>One radiator in the flat has no thermostatic head, because it lives in the bathroom and
bathrooms are allowed to be warm. It turned out to be the most informative device in the
heating system. With nothing to throttle it, its room temperature is a clean copy of whatever
the building's boiler room is doing: a rise every morning from half past five, a sag every
evening from ten. Two weeks of it and the schedule was obvious. The building heats for about
sixteen and a half hours a day and then goes to bed, like a sensible organism.</p>
<figure style="margin:1.5em 0">
<img loading="lazy" src="https://jan.zorz.si/HA-automations/bathroom-supply.svg" alt="Bathroom temperature over 24 hours for each of seven days and their average: flat overnight, a rise from half past five, a plateau through the day, a fall after ten in the evening" style="width:100%;border-radius:12px">
<figcaption>Seven days of the bathroom, one line each, and their average in bold. The shaded band is what we now call the supply window. Nobody told us the schedule; the radiator did.</figcaption>
</figure>
<p>This reframed two things we had been quite proud of. The night setback, which lowered the
targets at half past ten, was saving nothing, because there was nothing to save: the water had
already gone cold on its own. And the morning target, which rose at half past six, was an hour
late to a party that started at half past five. The day now begins when the building opens the
tap. The heads also stop being written to while the supply is off, since a command to a cold
radiator is a battery spent on a gesture.</p>
<p>There is now a sensor called "supply from the building", derived entirely from a bathroom.
It is right more often than the people who run the boiler room would probably like.</p>

<h2>Pre-heating a flat you are not in</h2>
<p>Coming home to a flat held at seventeen degrees for two months is not a hardship, exactly,
but the walls have opinions. Air warms in hours; a wall of this flat takes a day or two to stop
feeling like a wall. So the return is now a two-act play, and the date for both acts comes from
the travel calendar, which already knew when we were coming back and was slightly offended not
to have been asked.</p>
<p>Three days before arrival the whole flat drifts up to nineteen, so the masonry can start
thinking about it. Two days before, every room goes to its daytime target plus one degree, day
and night, no setback, until the door opens. Then the ordinary rules take over. If the plan
changes and nobody arrives, it all switches itself off the day after the date it was given,
because a flat warming itself for ghosts is the kind of thing this site exists to prevent.</p>
<p>Naturally the first version of this failed every hour for a night. It compared the current
time with a placeholder that said "no arrival", and the platform, helpfully, had turned the
placeholder into a number. The nightly reviewer found it before we did. We have learned to
reserve our sentinel values from the letters, not the digits.</p>

<h2>The regulator that ran every twenty-seven seconds</h2>
<p>While we were congratulating ourselves on an hourly regulation cycle, a weekly review
counted the actual runs: three thousand two hundred and forty-eight in a day. The living room's
temperature was an average of two sensors, one of which reports every thirty seconds and
rounds to two decimals, and the regulation was politely re-running on every twitch. Worse, it
occasionally caught a valve head between receiving a new offset and reporting a new reading,
doubled the correction, and corrected the correction thirty seconds later. Seven writes in
forty-nine seconds, to a device on three AA batteries, in a room whose temperature had moved
by a tenth of a degree.</p>
<p>The same sensor, it emerged, sits on the sofa and faces a window. On sunny mornings it
reported thirty degrees with great confidence while the other sensor in the same room reported
twenty. The fix was not clever: the shaded sensor leads, the sunny one is consulted only when
it agrees to within a degree, the result is rounded to a tenth, and the regulator runs every
fifteen minutes whether or not anything twitched. Writes to the heads fell from sixty a day to
roughly ten. The batteries have not sent a thank-you note, but we feel they would.</p>
<figure style="margin:1.5em 0">
<img loading="lazy" src="https://jan.zorz.si/HA-automations/head-offsets.svg" alt="Offset written to each of four radiator heads over a week: flat before the season, jumping negative when the radiators first run hot, then jittering several times an hour until the regulator was calmed, then stepping rarely" style="width:100%;border-radius:12px">
<figcaption>The offsets the house writes into the four heads. The first jump is autumn: the radiators run hot and the heads start reading high. The jitter afterwards is us, correcting the correction every thirty seconds. The calm after the second line is the fix.</figcaption>
</figure>
]]></content:encoded>
  </item>
  <item>
    <title>The bank installed a sensor in my wallet</title>
    <link>https://jan.zorz.si/HA-automations/money-telemetry.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/money-telemetry.html</guid>
    <pubDate>Fri, 14 Aug 2026 12:00:00 +0200</pubDate>
    <description>Card payments as a telemetry stream: two capture paths, a watchdog on the capture, and the discovery that a bank's text message is a perfectly good sensor.</description>
    <category>data capture</category>
    <content:encoded><![CDATA[
<p>At some point the bank started sending a text message for every card payment, which is to
say the bank installed a sensor in my wallet and forgot to charge for it. A message arrives, a
parser files it, and spending became a stream like temperature or particulates: graphable,
alertable, and, like every other stream in this house, most interesting on the day it silently
stops.</p>
<p>A bank's text message is, if you squint, a sensor. It fires on an event, it carries a value and a timestamp, and it is remarkably reliable right up until the day the bank changes its wording. So we treat it as one, with a watchdog on the sensor, because a spending log that quietly stops logging is worse than none.</p>

<h2>Two paths, because one is a rumour</h2>
<p>The texts are captured twice, on purpose. An automation app on the phone posts each one to a
webhook the moment it arrives; the same app also forwards it to a mailbox, which is read on a
schedule. The two paths fail differently, which is the entire point: the webhook needs the
house to be up right now, the mailbox survives downtime and delivers late.
<a href="https://jan.zorz.si/HA-automations/redundancy.html">The redundancy page</a> covers the theory; this is the theory holding
real money.</p>
<p>A third path catches what the texts structurally cannot: some payments never generate a
message at all, the bank texts one card's transactions and merely app-notifies another class
of them, so the phone's wallet and banking notifications are captured as their own stream and
parsed like the texts are. It exists because the
monitoring reported spending the first two paths could not have seen, which is the redundancy
lesson arriving in person.</p>
<p>Records are deduplicated on a natural key (timestamp, amount, card), and each record keeps a
note of which paths saw it. A payment seen twice is not a duplicate. It is a payment with a
healthy escort.</p>

<h2>A watchdog on the wallet feed</h2>
<p>The failure mode of a spending tracker is not a wrong number, it is a smaller one. The bank
rewords its messages, an OS update revokes a permission, a forwarding rule expires, and the
ledger stops growing, which looks exactly like a frugal week. Nobody audits good news.</p>
<p>So the capture itself is monitored. If one path keeps seeing payments the other has stopped
recording, that is a broken parser. If nothing at all arrives for longer than the longest real
gap in the ledger's own history, that is a broken pipeline, however calm it looks. &ldquo;A
quiet month&rdquo; is now a claim that requires evidence, like everything else in this
house.</p>
<p>That watchdog earned an erratum within its first weeks, and the manner of it belongs here. When the capture grew from two paths to four, the paths were renamed; the watchdog kept
counting the old name, which nobody wrote any more, and duly reported ten consecutive misses
on a path that was healthy under its new name. A watchdog watching a ghost. Worse, its
degraded state no longer contained the word its own alarm was listening for, so it counted
faithfully and complained to nobody.</p>
<p>Both surfaced not through any monitor but through the
question &ldquo;do we <em>actually</em> receive all of this, on every path?&rdquo;, answered
by checking arrivals instead of design. The fixes: the watchdog now judges the paths that
exist, every degraded state carries the alarm's trigger word, and the wallet's credit no
longer depends on which of two notifications wins a three-second race. Rename a path and you
have two systems to update; the one that watches is the one everyone forgets.</p>

<h2>The security section nobody screenshots</h2>
<p>Here is the awkward part. The webhook receiving these messages cannot be meaningfully
authenticated: it has to accept posts from a phone out in the world, and a secret embedded in
a public page's JavaScript is a secret in the way a note taped to the door is a lock.
Pretending otherwise would be decoration. The honest model is that the endpoint is reachable,
and everything arriving on it is hostile until proven boring.</p>
<p>So the defence lives at the two ends of the data's life: every string is sanitised on the
way into storage, and escaped again at every point where the dashboard renders it. Both ends,
because the failure being prevented is not graffiti on a chart.</p>
<p>A payment description is
attacker-controlled text; whoever names a card terminal decides part of what my dashboard will
try to display, and an unescaped render of that is how a spending chart becomes a way into the
platform that runs the alarm. Cutting the chain twice means one mistake is survivable.</p>

<h2>What it earns</h2>
<p>Categories are assigned by rules and corrected by hand, the monthly shape emerges, and the
building-management bills, which arrive on paper and leave by bank transfer, a channel no text
message ever sees, get their own importer and a months-behind counter. &ldquo;Did I pay
those?&rdquo; stopped being a feeling and became an integer with an alert on it.</p>

<p>The household now knows what it spends with the same precision it knows the bedroom CO&#8322;. Whether this is an improvement in quality of life is a question we have agreed not to ask.</p>
<div class="win"><b>Money is a sensor.</b> It gets the same treatment as every other one in
the house: independent capture paths, a freshness check, honest gaps instead of confident
silence. The graphs are optional. Noticing the silence is not.</div>

<h2>The bank turns off the text messages</h2>
<p>At the end of September the bank stopped sending text messages about card purchases and
moved them into its app, without consulting the household's automation. The feed went quiet
in the way that this site keeps warning about: no error, no notice, just an absence. The
watchdog that judges capture paths noticed within a day, which is the only reason this
paragraph is not longer.</p>
<p>The replacement arrives as a phone notification, and a phone notification is a much worse
witness than a text. It carries no time, no card number and no original currency, just a
sentence and a balance. So the sentence is now collected four separate ways, and every way lies
in its own dialect.</p>
<ul>
<li>The phone's automation app forwards it directly. Reliable, until the phone has no
signal, at which point it used to shrug and drop it. It now retries for an hour and sends the
notification's own timestamp along, so a purchase that arrives two hours late is filed under
the hour it happened.</li>
<li>The home platform's own phone app forwards the last notification it saw. Also the last
one only. When two notifications of the same app pile up, the operating system folds them into
a group with no text, and the app forwards the fold. The cure was embarrassingly physical: the
automation app now deletes each notification ten seconds after forwarding it, so there is never
a pile.</li>
<li>The phone's payment app, which knows the card and the original currency but not the
merchant's full name. It spells the pharmacy with one kind of quotation mark. The bank uses
another. For one day that difference was enough to make one coffee into two.</li>
<li>E-mail, as the slow and stubborn fallback. It also transliterates the letter <i>ž</i>
into a question mark, which the parser read as "the merchant's name continues", and so the
merchant's name continued, all the way through the available balance.</li>
</ul>
<figure style="margin:1.5em 0">
<img loading="lazy" src="https://jan.zorz.si/HA-automations/capture-paths.svg" alt="Matrix of card transactions over three weeks against five capture paths, a dot where a path delivered the transaction: text messages stop at the start of October, the other four fill in with gaps" style="width:100%;border-radius:12px">
<figcaption>Three weeks of card transactions, one column each, against the five ways a transaction can reach the ledger. The text-message row ends where the bank stopped sending them. No single row is complete. Together they are.</figcaption>
</figure>
<p>Two special cases deserve a mention. A declined withdrawal produces a notification whose
text is identical to the successful withdrawal a minute later; only the title differs, and for
a day the ledger contained a purchase made by nobody with no card. And online purchases, which
never touch the payment app, turn out to announce their original amount in the one message
we had been ignoring: the request to confirm the payment. The parser now keeps that for half
a minute and marries it to the purchase when it lands.</p>
<div class="win"><b>Four witnesses, one truth.</b> No single path is trusted; each one is
asked to agree with the others, and the ledger records which ones showed up. The interesting
information is still in who stayed silent.</div>
]]></content:encoded>
  </item>
  <item>
    <title>What the hardware actually is</title>
    <link>https://jan.zorz.si/HA-automations/hardware.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/hardware.html</guid>
    <pubDate>Thu, 13 Aug 2026 12:00:00 +0200</pubDate>
    <description>The real bill of materials: names, versions, and a verdict on each, including the ones we would not buy again and the one that is quietly the best thing in…</description>
    <category>bill of materials</category>
    <content:encoded><![CDATA[
<p>Most home-automation writing stops at "use Zigbee, avoid cloud". This is the actual bill of
materials of one installation, which differs from the shopping list in one important way: it
includes the verdicts, after living with each part. Some we would buy again tomorrow. Some we
keep out of stubbornness. One we keep because throwing it away would mean admitting something.
Numbers as of writing: <b>Home Assistant 2026.9</b>, <b>1,623 entities</b>, <b>177
automations</b>, roughly <b>65,600 lines</b> of our own code and configuration, two
dwellings.</p>

<h2>The brain</h2>
<p><b>Home Assistant Green.</b> 4&nbsp;GB RAM, 28&nbsp;GB eMMC. It runs the whole thing
comfortably, but both numbers are the actual constraint on what else can live here, and
both have bitten.</p>
<ul>
<li>The 4&nbsp;GB fills to about 2.4&nbsp;GB used at idle, measured on the host across the OS,
supervisor, core and add-ons, before any diagnostic tooling. When it runs out, the kernel swaps to
eMMC and everything crawls. The tell is specific: <em>slow over the network, fast on the local
API</em> usually means swap, not an application fault.</li>
<li>The 28&nbsp;GB is shared by the database, backups and add-ons. One runaway automation wrote
6.4&nbsp;GB of log events overnight and filled it. A package-manager cache quietly held
2.7&nbsp;GB for months.</li>
</ul>
<div class="note"><b>Buy the bigger box.</b> Not because the software needs it, but because
every diagnostic tool you will want to run (a second container, a language model
session, a database vacuum) needs headroom the base model does not have. Vacuuming the
database requires free space equal to the database, so it fails exactly when the disk is
full.</div>

<h2>Zigbee</h2>
<p><b>Zigbee2MQTT 2.14</b> with a <b>ZBT-2</b> coordinator. 32 devices: 18 routers, 14 end
devices. Twenty of the 32 are IKEA, and the count grows by one most months.</p>

<h3>The ZBT-2, honestly</h3>
<p>Day to day it is a good, stable coordinator, the mesh runs for weeks without a
thought, and none of the incidents elsewhere on this site trace back to it. It has one
documented fault worth knowing about: occasionally it throws <code>ASH_NCP_FATAL_ERROR</code>
and drops the network until it is restarted.</p>
<p>What makes this worth reporting rather than shrugging at: the fault appeared on
<strong>two separate ZBT-2 dongles</strong>. Same symptom, different physical unit. That rules
out a bad sample and points at what both units share: the firmware, or, as a reviewer
correctly noted, the host's USB path and its driver, which this test cannot separate.</p>
<p>Firmware
remains the favourite (USB power and the disk were ruled out before the second dongle went
in), and the planned upgrade below doubles as the experiment that settles it, which is the
good news either way, because firmware is fixable, whereas a hardware fault would mean the
model is just bad.</p>
<p><em>Next step here: a coordinator firmware upgrade is planned, and this page will be updated
with whether the fault goes away. If you run a ZBT-2 without ever seeing this error, nothing is
wrong with yours, it is intermittent, weeks apart, and recovers with a restart.</em></p>

<h3>IKEA as the backbone: boring, and that is the point</h3>
<p>INSPELNING and TRETAKT smart plugs, TRADFRI bulbs, VINDSTYRKA air sensors, all mains
powered, so all routers. That is what makes the mesh: most of the routers here are IKEA,
bought for other reasons, quietly holding the network up.</p>
<p>The end devices are the same story: PARASOLL door contacts, VALLHORN motion, BADRING water
leak, RODRET dimmers. Cheap, dull, and none of them has been the cause of an incident on this
page. The VINDSTYRKA in particular does more than its price suggests, its particulate
channel turned out to be a reliable cooking detector, which is not what it was sold as.</p>

<h3>Aqara: works, but it is picky about company</h3>
<p>One LUMI device here (a water sensor) among 32. Aqara end devices are known for choosing a
parent router and then dropping off if that parent is not to their taste, and non-Xiaomi routers
frequently are not. If your mesh is built from another brand, expect this to be the device that
mysteriously goes offline. It is not a reason to avoid them, it is a reason to pair them
near the coordinator and watch them for a week.</p>

<h3>Tuya: the local setup with a cloud renewal clause</h3>
<p>Several Tuya-based plugs, controlled locally. Local control needs a per-device key, and that
key <strong>changes every time the device is re-paired</strong>, after a factory reset or
a Wi-Fi change. The only way to retrieve the new one is the vendor's cloud API.</p>
<p>So the cloud account has to be kept alive indefinitely, unused, purely so that local-only
operation stays recoverable. Delete it and the next reset makes the device uncontrollable, local
network or not.</p>

<h2>The two meshes, photographed</h2>
<p>Both networks, as their own tooling draws them. The interesting part is not either picture, it is that the two tools choose to show you <em>different things</em>.</p>

<figure>
  <img loading="lazy" src="https://jan.zorz.si/HA-automations/zigbee-map.png" alt="Zigbee2MQTT network map: the mesh around a coordinator"
       loading="lazy">
  <figcaption><b>Zigbee, as Zigbee2MQTT draws it.</b> Line colours are
  <em>relationships</em>, not signal: <span style="color:#d0342c">red</span> = that neighbour
  is the node's <em>parent</em>, <span style="color:#15803d">green</span> = its <em>child</em>,
  <span style="color:#6366f1">blue</span> = a <em>sibling</em>, two routers that peer
  with each other. The numbers are link quality (LQI, out of 255), one per direction.
  Two things worth noticing: the coordinator is <em>not</em> the busiest node, an
  air-quality meter that happens to be a router carries the middle of the mesh, and the
  battery sensors hang off whichever mains-powered plug was nearest, which is why buying
  boring mains-powered devices quietly builds your network for you.</figcaption>
</figure>

<figure>
  <img loading="lazy" src="https://jan.zorz.si/HA-automations/thread-map.png" alt="Thread topology: border router, leader bulb, routers and sleepy end devices"
       loading="lazy">
  <figcaption><b>Thread, as the border-router panel draws it.</b> Here the colours mean
  <em>link quality</em>, not relationship, green is a healthy link, orange weaker, red
  dashed poor or aging. Roles are the icons instead: the crown is the network
  <em>leader</em>, a role the mesh elects by itself, and it elected <strong>the light
  bulb in the office</strong>. The circular arrows are routers: both smart plugs and the
  USB-powered air monitor route for everyone else, exactly the &ldquo;mains devices carry the
  mesh&rdquo; rule again. The <code>zzz</code> badges are sleepy battery devices that wake to
  report and vanish.</figcaption>
</figure>

<div class="win"><b>The observability difference is the real lesson.</b> Zigbee2MQTT tells you
<em>who depends on whom</em> (relationships) and leaves quality as numbers; the Thread panel
tells you <em>how good each link is</em> and leaves the dependencies to icons. Neither shows
both at once. When a device misbehaves, you will want both answers (who is it attached
to, and how well) so knowing which map answers which question saves a debugging round.</div>

<h2>Thread and Matter</h2>
<p>Both in use, with one strong preference: <strong>Matter-over-Thread rather than
Matter-over-Wi-Fi</strong> (a sentence in which the word "over" was quietly doing two different
jobs, and on reflection deserved untangling), and the reason is not radio, it is your network. If your IoT devices sit on their own VLAN (which is standard advice, and correct) then Matter over Wi-Fi has to cross that boundary,
and it relies on multicast DNS, which does not.</p>
<p>Thread devices reach the controller through a
border router and sidestep most of the problem: the border router itself still advertises the
devices over mDNS on the infrastructure side, so it has to sit on, or be reflected into, the
controller's segment. Reflecting for one border router beats reflecting for every bulb.</p>
<p>Solving it the other way needs an mDNS reflector between segments, and plenty of consumer
routers do not have one. The workaround here is a small avahi container doing nothing but
reflecting.</p>

<h2>Infrared: the honest failure</h2>
<p>A <b>Broadlink RM4 mini</b> driving an air conditioner through SmartIR, with the remote
captured by hand. It works, and it is the single most dangerous device in the house from an
automation standpoint, because <strong>infrared is one-way</strong>. The system knows what it
sent, never what happened.</p>
<p>Worse, on this handset the captured <code>off</code> command is a <em>power toggle</em>, the same frame for on and off. An automation written to guarantee the air conditioning was off
in an empty flat started it instead, for four hours.
<a href="https://jan.zorz.si/HA-automations/verified-wrong.html">The full post-mortem is here.</a> If you automate IR, verify the
power command against a known toggle frame, and pair every command with an independent sensor
that can confirm the outcome. Ours is now a tilt sensor glued to the louver, which turned the
honest failure into a <a href="https://jan.zorz.si/HA-automations/ac-loop.html">closed loop</a>; the section title stays, as a
memorial.</p>

<h2>Appliances: one excellent, one hopeless</h2>
<table>
<tr><th></th><th>Dishwasher</th><th>Washing machine</th></tr>
<tr><td>Appliance's own path</td><td>Local WebSocket on the LAN</td><td>Vendor cloud only</td></tr>
<tr><td>Second, local path</td><td colspan="2">A metering smart plug, on both</td></tr>
<tr><td>Detail from the appliance</td><td>Programme, phase, progress, remaining time, salt,
rinse aid, faults</td><td>Cycle counters, selected options</td></tr>
<tr><td>Works with the internet down</td><td>Yes</td><td>Yes, via the plug, not the
appliance</td></tr>
<tr><td>Verdict</td><td>Excellent, but a manual, unofficial install</td>
<td>Cloud unreliable; the plug carries it</td></tr>
</table>
<p>The dishwasher's local integration is genuinely good and gives more than the vendor's own
app. It also once <em>hung Home Assistant's startup</em>: unreachable while nobody was home, its
connect loop was registered as a tracked task, so the startup phase waited for it, timed out,
and abandoned the webhook and push registrations that were still finishing. Every notification
path went quiet, silently, and only while away. The fix was one word, a background task
instead of a tracked one.</p>
<p>The washer, an AEG L9WAN61BCE, has <strong>no local API at all</strong>; a promising-looking library turned out to
be for portable air conditioners. Its cloud reports the appliance as <em>Off</em> mid-cycle,
because it disconnects and the last known state keeps being served. We eventually
<a href="https://jan.zorz.si/HA-automations/washer.html">listened to an entire wash at the router</a> to find out exactly what it
says to its maker, and the answer, in the end, was a door rather than a wall.</p>

<h3>The metering plug does most of the job anyway</h3>
<p>This is the part worth taking away, because it applies to any appliance a vendor has decided
not to open up. A <strong>smart plug that measures power</strong>, not just switches, recovers most of what the missing API would have given:</p>
<ul>
<li><b>Running or not</b>, from the current draw, with hysteresis. This is more truthful than
the appliance's own status: the plug can be switched on while the machine sits idle, and the
cloud can claim <em>Off</em> while the drum is turning.</li>
<li><b>Cycle boundaries and energy per cycle</b>, which is what a &ldquo;finished&rdquo;
notification actually needs.</li>
<li><b>Wear, months before failure, with caveats.</b> With volts and amps as well as watts you
get the power factor. On a plain induction motor a drying run capacitor drags it down early; a
worn bearing pushes it the <em>other</em> way and belongs in energy-per-cycle instead; and an
inverter-driven appliance shows the wall its rectifier, not its motor. So treat the ratio as a
per-programme fingerprint whose drift means "something changed", not as a bearing diagnosis.
Watts alone still show nothing until the day it dies.</li>
</ul>
<div class="note"><b>One trap in that last one.</b> Averaging the power factor across a whole
cycle mostly measures <em>which programme you chose</em>, because both machines spend most of a
cycle running a heating element, and a heater is purely resistive. Compute it only from samples
in a power band that contains the motor and excludes the heater, then a short cold wash
is comparable with a long hot one. Test it with a synthetic trace before trusting it: feed a
cycle whose motor runs at 85&nbsp;% and whose heater runs at 99&nbsp;%, and check the answer
comes back near 85 rather than near 95.</div>
<p>So the honest verdict is not &ldquo;the washer is unautomatable&rdquo;. It is that the
appliance's own interface is decoration, and a metering plug (a few euros, no vendor
account, works with the internet down) carries the automation instead.</p>

<h2>Air quality</h2>
<p>IKEA <b>VINDSTYRKA</b> units for particulates and VOC, plus a formaldehyde-capable meter and a
Qingping monitor at the second property. Two lessons worth passing on:</p>
<ul>
<li><b>Particulates and VOC disagree usefully.</b> Cooking raises both. One specific indoor
activity raises formaldehyde while particulates stay flat. Selectivity came from the shape of
the signature, not from an expensive sensor.</li>
<li><b>The Qingping answers only its own subnet.</b> Ping works, the port is open, packets
demonstrably arrive, and the application-level query gets no reply, because it checks the
source address. Reaching it across a VPN needs source NAT on the far router, scoped to that one
device.</li>
</ul>

<h2>Audio and video</h2>
<p>A 2010 <b>Sony Bravia</b>, a <b>Cambridge Audio CXN100</b>, a Chromecast. The television has
no modern control API, the only open port serves a DLNA renderer descriptor, so the
integration is <code>dlna_dmr</code> and <strong>availability is the power signal</strong>:
reachable means on, unavailable means standby.</p>
<p>The audio link only establishes when the television itself wakes the amplifier. So the
automation's entire action is to turn the amplifier <em>off</em> and wait for the TV to wake it.
Every attempt to improve that by adding a power-on made it worse.</p>

<h2>Network</h2>
<ul>
<li><b>MikroTik RB750GL, RouterOS 6.</b> No REST API on this version, automation goes
through the binary API on port 8728, which works fine and is barely documented. Its web
interface served a certificate that had been expired for over two years before anyone looked.</li>
<li><b>ASUS RT-AX52.</b> Stock firmware has <em>no persistent script hook</em>, and any Apply in
the web UI wipes hand-added firewall rules and routes. Merlin is not an option on this chipset.
Everything it needs is therefore re-applied by a reconciliation loop every ten minutes, forever.</li>
<li><b>2&times; TP-Link TL-SG108E.</b> Managed switches with no real API; control is a UDP
protocol reverse-engineered by the community. It works, has no session limit, and the web UI's
polling errors are noise to be ignored.</li>
</ul>

<h2>The one subscription worth paying for is free</h2>
<p><b>healthchecks.io.</b> Three heartbeats, the automation platform and a router at each
site, pushed outward on a timer. If one stops arriving, an external service raises the
alarm. It is the only alert path that survives the house being dead, because it is the only one
not running inside it. Free tier, five minutes to set up, and it changes what the system is
capable of noticing at all.</p>


<h2>Since August: the things that arrived while this page was not looking</h2>
<p>A bill of materials is a snapshot, and a household with a soldering iron and a delivery
address is not a still subject. The following turned up in September and have already earned
their entries.</p>
<ul>
<li><b>A 3.5-inch touch display</b> in the living room, running an open display firmware over
MQTT. It shows the house spirit's face, its mood and its thoughts in a font readable from the
couch, dims by room illuminance, goes dark when nobody is there, and had to be talked out of
showing its IP address on every boot. The best 20 euros on this page.</li>
<li><b>Thermostatic radiator heads</b> on all four radiators, paired over the mesh the hard
way (mount first, calibrate, then pair). The plan was to feed them the room sensors we already
own; the heads preferred their own thermometer and a correction, so each one now regulates on
its internal sensor with an offset the house recomputes against the room sensor, while the
house sets the target: day, night, nobody home, window open. Out of season they sit fully open
and take a short walk once a week so the valves do not seize.</li>
<li><b>A motorised projector screen</b> on a mesh shutter switch with a broken bottom limit,
calibrated by stopwatch to stop at picture height instead of the floor. See the
<a href="https://jan.zorz.si/HA-automations/audio-blob.html">audio page</a> for the whole surprisingly satisfying story.</li>
<li><b>A colour bulb of the spirit's own</b>, switched from one protocol to the other with a
ritual of power cycles, and still advertising for the job it left — see
<a href="https://jan.zorz.si/HA-automations/matter-ghost.html">the ghost</a> — breathing in the mood colour on every change, in a
lamp that will wear a translucent head.</li>
<li><b>A tilt sensor glued to the air conditioner's louver</b>, which turned a one-way infrared
remote into a closed loop. Cheapest acknowledgement in the flat.
<a href="https://jan.zorz.si/HA-automations/ac-loop.html">The loop</a>.</li>
<li><b>A subwoofer controlled over Bluetooth</b>, with a loudness curve that has moved twice as
the driver loosened up, and two decibels more whenever the air conditioner's fan is occupying
the bottom octave.</li>
<li><b>A blood pressure cuff</b> that speaks the standard Bluetooth profile, refused to pair
twice, and hands over readings anyway to a passive listener the moment its button is pressed.
The house now knows the household's blood pressure, which is more than the household's doctor
can say between visits.</li>
<li><b>An HDMI matrix</b>, four in, two out, with an eARC port that took the audio job away
from the television and a remote we taught the house one button at a time. Two sources, two
screens, four scenes and an End. The <a href="https://jan.zorz.si/HA-automations/audio-blob.html">audio page</a> has grown a
matrix.</li>
<li><b>A metering plug under the projector</b>, never switched off, whose only job is to
witness the thirty seconds after the on-code and say whether a 2008 lamp actually struck.</li>
<li><b>An air purifier</b> with a local interface, switched on by the indoor smoke detector
from the <a href="https://jan.zorz.si/HA-automations/detection.html">detection page</a>, by cooking, or by the room's own
particulate sensor, and regulated on the trend rather than the level: up a step when the air
worsens, down when it clears, straight to the quiet step when it is already clean, and full
speed only when nobody is in the room to hear it. Off again twenty minutes after the air is
clean.</li>
<li><b>A Thread temperature sensor</b> for the back room, and a second two-rocker button for
the scenes above.</li>
<li><b>A thumb-sized ESP32 board by the bed</b>, running ESPHome: a Bluetooth proxy by trade
and, since the night described on <a href="https://jan.zorz.si/HA-automations/blood-pressure.html">the cuff page</a>, the cuff's
own reader, because a board that does the handshake locally beats a server that does it over
Wi-Fi. Its red power light cannot be switched off by any software on Earth; see
<a href="https://jan.zorz.si/HA-automations/traps.html">the field guide</a>.</li>
</ul>
<p>Verdicts, in the spirit of this page: buy the display, buy the tilt sensor, buy the cuff if
you already own the arm, and buy the matrix the day you own a second screen. Buy the small board,
and give it one job rather than two. The radiator heads
are fine once you stop arguing with their thermometer. The purifier is quieter than the
argument about which speed it should run at. The bulb ritual is not a feature, it is a rite of
passage.</p>

<h2>What we would buy again, and what we would not</h2>
<table>
<tr><th>Again</th><th>Reluctantly</th><th>No</th></tr>
<tr>
<td>IKEA mains-powered anything, the mesh builds itself<br><br>
VINDSTYRKA, outperforms its price and its stated purpose<br><br>
Bosch with the local integration<br><br>
Smart plugs with current metering, the most useful sensor per euro in the house</td>
<td>ZBT-2, good and stable day to day; one intermittent firmware fault, upgrade
pending, verdict to be updated<br><br>
Tuya, fine locally, as long as you never delete the cloud account<br><br>
Aqara, good hardware, fussy about its parent router</td>
<td>Any appliance whose only interface is a vendor cloud<br><br>
Infrared for anything that matters, unless you can measure the result<br><br>
The 4&nbsp;GB box, if you intend to debug it while it runs</td>
</tr>
</table>

<p>The general rule that fell out of all this: buy the thing with the boring radio and the open protocol, and be suspicious of anything whose best feature is the app.</p>
<div class="win"><b>The pattern across all of it:</b> the parts that caused trouble were rarely
the cheap ones. They were the ones with no way to confirm what had actually happened, one-way infrared, a cloud that reports the last state it saw, a switch with no API. Metering
plugs cost a few euros and are the most quoted devices on this entire site, precisely because
they measure instead of assert.</div>

<h2>What we did not buy, with reasons</h2>
<p>A smart safe, with an app and a lock and notifications and twelve kilograms of mass. It
has no burglary rating at all, which makes the app a way of being informed promptly that your
jewellery has left. A certified safe bolted into concrete with a cheap vibration sensor taped
to the outside does the same job in the home platform and a much better job as a safe.</p>
<p>Three blood-pressure cuffs, compared not by accuracy first but by whether they speak the
standard Bluetooth profile. One does, and can be read by a microcontroller on the sofa, as the
existing one is. One is clinically the best of the three and speaks a private protocol known
only to its own app and a handful of patient volunteers with packet sniffers. One is cheap,
speaks the standard, and has never been clinically validated by anyone, which for a device
whose entire purpose is a number seemed disqualifying. We chose the boring one.</p>
<p>And the fitness watch, which the household would like to read without the maker's cloud.
The protocol has been reverse-engineered by volunteers and a library exists that streams the
current numbers over Bluetooth to a computer, provided the watch is disconnected from the phone,
because it holds one connection at a time. History still travels only as files, and nobody has
written the file transfer yet. We are watching the project the way one watches a kettle.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Systems that repair themselves, and protect themselves</title>
    <link>https://jan.zorz.si/HA-automations/self-healing.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/self-healing.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>Transient breakage is common and mostly repairable without a human. The trick is knowing which faults to fix silently and which ones a human genuinely needs…</description>
    <category>resilience</category>
    <content:encoded><![CDATA[
<p>The difference between a demo and a system you live with is what happens on the bad days.
A surprising amount of home automation breakage is <em>transient</em> (a wedged handshake, a
route that vanished, a token that expired) and can be repaired automatically if something is
willing to check and try again.</p>
<p>Most breakage in a house like this is transient: a library that could not reach its cloud for a minute, a radio that missed a heartbeat, a container that came back a second late. Fixing these by hand is tedious; fixing them automatically is easy; knowing which ones to fix automatically and which ones to shout about is the whole craft.</p>

<h2>Repair before you notify</h2>
<p>The shape that works, over and over:</p>
<pre><code>detect a bad state
  → attempt the repair
  → wait
  → re-check
      recovered → stay silent (or log quietly)
      still bad → now escalate, and say what was tried
</code></pre>
<p>The final clause matters. An alert that says "this is broken and the automatic recovery
did not fix it" is far more actionable than one that just says "this is broken", because it
has already ruled out the easy explanation.</p>

<h2>Things worth healing automatically</h2>
<ul>
<li><strong>Wedged audio handshakes.</strong> Some consumer AV links get stuck in a state where
everything reports connected but no sound flows. A watchdog that notices the impossible
combination (panel on, amplifier awake on the right input, but not actually playing) and
re-runs the wake sequence turns a recurring annoyance into nothing.</li>
<li><strong>Half-dead local connections.</strong> A device WebSocket can stop delivering
updates mid-cycle without dropping. Detect staleness, reload just that integration.</li>
<li><strong>Routes and firewall rules on other devices.</strong> Some routers forget hand-made
state on every reboot <em>and</em> on ordinary config changes. Rather than fight it, re-assert
the desired state on a schedule and treat drift as normal. The check is read-only when nothing
is wrong, so running it often is free.</li>
<li><strong>Sockets that must never be off.</strong> If the router lives on a smart plug, an
interrupted script can leave the home without internet and no way to fix it remotely. A rule
that switches it back on after a couple of minutes is trivial and has saved the situation.
Two provisos: the plug must not depend on the router it feeds (Zigbee or Thread, never Wi-Fi
or anything cloud), and give the re-on rule a retry bound, a plug that keeps failing has a
reason, and it deserves attention rather than an infinite argument.</li>
<li><strong>Frozen polled sensors.</strong> They keep their last value forever. Reload the
integration; asking politely for an update does nothing.</li>
<li><strong>Escalating internet recovery.</strong> Restart the link; if that fails, power-cycle
the router; if that fails, retry on a long cycle and flag that the fault is probably upstream.
Each step is more disruptive than the last, so try them in order.</li>
</ul>

<div class="note"><b>Every self-healing loop needs a stuck detector.</b> Recovery mode itself
can get wedged, retrying forever, achieving nothing, telling nobody, like a very patient employee
who has not noticed the building is empty. Watch for "we have been in recovery for more than N
hours" and escalate that as its own fault.</div>

<h2>Self-protection: the system defending itself from itself</h2>
<p>Automation can also damage its own host. Three mechanisms worth having.</p>

<h3>A circuit breaker for runaway logging</h3>
<p>An automation that errors in a loop can generate hundreds of thousands of log events an hour,
inflate the database, and fill the disk. By the time you notice, the machine is already
struggling.</p>
<p>Measure errors-per-minute per source. Above a threshold, <strong>automatically disable the
offending automation</strong> and say so. This is prevention rather than notification: it stops
the damage while you are asleep.</p>
<p>Two details make it safe. Keep an explicit <strong>protect list</strong> of automations that
must never be auto-disabled, anything touching alarms, leaks, fire or heating. And key that
list on structural metadata, an explicit entity list or, better, a registry label, never on
name substrings: a fuzzy pattern once protected a hallway dimmer while
leaving a water heater unprotected, purely by accident of naming.</p>

<h3>A storage watchdog that acts, not just warns</h3>
<p>A recorder database that grows ten-fold overnight is a symptom whose consequence is a full
disk hours later. Watch the database size, and above a threshold run a shortened purge
automatically. Warning is not enough when the failure mode is unattended and time-boxed.</p>
<div class="note"><b>Check free space before compacting.</b> Reclaiming space usually needs a
temporary copy roughly the size of the database. On a nearly-full disk that either fails or
finishes the job the runaway started. Purge without compaction is safe; compaction is not.</div>

<h3>Absolute failsafes on anything that heats</h3>
<p>For a high-power heating load, have a last-resort cut-off independent of the clever logic, and make it restart-proof, since the clever logic is exactly what tends to be reloaded during
troubleshooting. Then <strong>verify the cut actually happened</strong>: wait, re-read the state,
and if the appliance is still drawing power, escalate loudly instead of reporting success. A
safety mechanism that assumes its own command worked is not a safety mechanism. It is a prayer
with a log entry.</p>

<h2>Latch what the API forgets</h2>
<p>A small trick with wide application. Many appliance integrations clear useful fields the
moment a cycle ends, the programme name becomes unknown at exactly the moment you want to
say "your cottons finished". Latch the value into your own storage while the cycle runs, and
read the latch afterwards. The same applies to meter readings you want to difference across a
cycle: capture the baseline at the start, because afterwards it is too late.</p>

<p>A system that heals everything silently is not resilient. It is a system with a secret. We try to keep ours to the harmless kind.</p>
<div class="win"><b>Ask of every recurring annoyance:</b> could something have noticed this and
tried the obvious fix before I did? Surprisingly often the answer is yes, and the fix is three
lines and a re-check.</div>

<h2>Updating things from five hundred kilometres away</h2>
<p>The password vault said an update was available, daily, for a week. We were sure we had
installed it. We had installed something; the vault was still on the old version, because a
locally built add-on never offers an update button, it only offers a reminder. It is now
updated, with a backup taken first and the version confirmed afterwards from outside, which is
the order of operations we recommend to anyone who has ever been sure.</p>
<p>The platform itself released a new major version the same week. We did not install it. A
point-zero release, a known bug in the phone app, three custom integrations touching an API the
release had tightened, and nobody at home to press a button if the box fails to come back. The
decision to defer is not laziness; it is the knowledge that a remote fix needs the thing to boot.</p>
<p>The traffic service, meanwhile, started refusing the platform's library because it did not
like the shape of its TLS handshake. The fix is a newer library, which ships in the next
platform version, which we are not installing. So the newer library was installed by hand, the
version pin was edited to match, and a note was left explaining that the next update will erase
all of this on purpose. Self-healing, in this case, means knowing which repairs are supposed to
disappear.</p>
]]></content:encoded>
  </item>
  <item>
    <title>A field guide to specific traps</title>
    <link>https://jan.zorz.si/HA-automations/traps.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/traps.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>A field guide to the specific, named, reproducible ways this system has bitten its owners. Each one cost an afternoon; reading about it costs a minute.</description>
    <category>debugging</category>
    <content:encoded><![CDATA[
<p>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.</p>
<p>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.</p>

<h2>Configuration that validates but does nothing</h2>
<p>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.</p>
<div class="win"><b>Habit:</b> after creating any remote job, <em>run it once</em> and check
its output. Creation success is not execution success.</div>

<h2>The self-feeding log loop</h2>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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.)</p>
<p>The fix that has held is not a better place for the template. It is <strong>no template
at all</strong>: 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:</p>
<ul>
<li>The script <strong>always exits 0</strong>. A failing shell command gets logged as an
error, which would feed the same loop through the back door.</li>
<li>A <strong>rate cap</strong>: 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.</li>
<li>Its own name stays <strong>in the ignore list</strong>, so this machinery's failures can
never become evidence for itself.</li>
</ul>
<p>The general shape: when a mechanism keeps finding new ways to feed itself, stop guarding
the paths and remove the food.</p>

<h2>Containers that are "started" with nothing running inside</h2>
<p>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.</p>
<p>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.)</p>

<h2>Polled sensors that freeze at their last value</h2>
<p>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.</p>
<p>Worse, the obvious remedy, asking the platform to update the entity, can return success
without doing anything. Only reloading the whole integration recovers them.</p>
<div class="win"><b>Defence:</b> a staleness detector comparing each polled entity's last-report
time against its own interval, with automatic reload as the remedy. And put that detector
itself into your external heartbeat's health list, because it can freeze too.</div>

<h2>The nuclear reload</h2>
<p>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.</p>

<h2>Encoding mismatches that fail completely silently</h2>
<p>A webhook receiving a JSON body sent with a form content-type: the entire document arrives
as a single field <em>name</em> with an empty value. Every lookup returns nothing and the
request looks empty rather than malformed.</p>
<p>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.</p>

<h2>One template error in a variables block kills the whole run</h2>
<p>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.</p>

<h2>Editing a config file by pattern match</h2>
<p>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.</p>
<div class="win"><b>Habit:</b> read raw bytes before patching, and scope every replacement to
the block you mean, extract it, edit it, put it back.</div>

<h2>Aggregations that hide a broken member</h2>
<p>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.</p>

<h2>The watchdog that counted correctly and could not complain</h2>
<p>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.</p>
<p>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.</p>

<h2>Cache-busting you forgot to bust</h2>
<p>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.</p>

<p>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.</p>
<div class="win"><b>The common thread:</b> every trap here is a component answering a question
it no longer understands. The cure is never a cleverer question. It is asking something that
cannot lie: the port, the process, the raw bytes, the clock.</div>

<h2>Reloading a template is an event</h2>
<p>Reloading template entities recreates them; for a moment each one is <code>unavailable</code>
and then back to its value. A state trigger written as <code>to: 'on'</code> fires on that return,
because <code>unavailable &rarr; on</code> is a transition to "on".</p>
<p>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 <code>from:</code> 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.</p>

<h2>A daylight edge that never comes back</h2>
<p>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.</p>
<p>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 <em>same</em> level sensor,
and the earlier lesson on <a href="https://jan.zorz.si/HA-automations/level-vs-edge.html">edges</a> gained a worked example.</p>

<h2>Orphans that are not</h2>
<p>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.</p>
<p>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.</p>

<h2>One owner per Bluetooth address</h2>
<p>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. <a href="https://jan.zorz.si/HA-automations/blood-pressure.html">The cuff page</a> has the whole night.</p>
<div class="win"><b>Habit:</b> for static advertisers, poll the timestamp of the last thing heard
instead of waiting to be called. And remember that the connection path is chosen on signal
strength alone, so "which scanner heard it" and "which scanner is talking to it" are different
questions with different answers, and the log usually prints only the first.</div>

<h2>A switch for a lamp on the power rail</h2>
<p>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.</p>
<div class="win"><b>Habit:</b> before writing software for a light, find out whether the light
has a wire to the software.</div>

<h2>Two memories written at different times</h2>
<p>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.</p>
<p>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.</p>

<h2>Locking the owner out, for safety</h2>
<p>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.</p>
]]></content:encoded>
  </item>
  <item>
    <title>How calm was the night</title>
    <link>https://jan.zorz.si/HA-automations/night-calm.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/night-calm.html</guid>
    <pubDate>Sun, 06 Sep 2026 12:00:00 +0200</pubDate>
    <description>A sleep score computed from the house's own sensors, with the wearable on the wrist admitted only as a witness that testifies when it was actually there.</description>
    <category>sleep</category>
    <content:encoded><![CDATA[
<p>The wearable on the wrist gives a sleep score every morning, and it is not wrong. It is also
not the house's. It knows nothing about the door to the bathroom, the CO&#8322; in the bedroom at
three in the morning, the light in the hall, or the fan that ran all night because the room was
too warm.</p>
<p>The house had been watching us sleep for years without drawing any conclusions, which is either discretion or a missed opportunity. We decided it was the latter, and taught it to grade the night.</p>
<p>The house knows all of that and had been throwing it away. So now, at a quarter past
six and again when the sleep flag drops, a script reads the night back out of the recorder and
scores it.</p>

<h2>What counts as a bad night</h2>
<p>The window runs from "went to bed", the sleep detector's flag capped at half past midnight,
to "got up", the flag dropping after four. Inside it the script counts things a person would
recognise as a bad night, each with a cost:</p>
<ul>
<li><b>Getting up.</b> Motion at the entrance is the way to the toilet, so bursts of it (plus
the bathroom door) are trips; the first one is free, every further one costs.</li>
<li><b>Restlessness.</b> The bedroom's presence radar holds "on" while somebody lies still;
each flip is somebody moving at the edge of its range or getting up.</li>
<li><b>Air.</b> Minutes above 1,500 and above 2,000 ppm of CO&#8322;, which a closed bedroom
with two people reaches by two in the morning.</li>
<li><b>Climate.</b> Average temperature above 24 degrees or below 17; humidity above 65 percent.</li>
<li><b>Light</b> in the hall at night, <b>duration</b> under six hours, <b>bedtime</b> after one.</li>
</ul>
<p>The wearable's score is blended in at thirty percent, but only when its bedtime belongs to
<em>this</em> night. It syncs in the afternoon, so at six in the morning it is usually still
telling us about yesterday, and a score from the wrong night is worse than none.</p>

<h2>The morning after</h2>
<p>The number feeds three places. The house spirit's ladder gets a morning branch: a night under
fifty makes it sleepy until ten, and its diary says why ("restless night: 3&times; to the
toilet, 25 degrees, light on twice"). The display's morning thoughts open with the verdict. And
a card on the dashboard shows the breakdown, so a bad score can be argued with.</p>
<p>The first night scored 67, "solid": two trips, a warm room, the hall light three times, and
perfect air because the window was open. The second scored 58 and blamed the heat again. We
believe the direction more than the number.</p>
<p>Twenty-six radar flips in six hours
might be a restless sleeper or might be the normal noise of two people turning over; a week of
logged nights will tell us which, and the thresholds will move. They are written down as a first
draft, in the code and on the page, so that nobody, including the nightly reviewer, mistakes
them for calibration.</p>

<h2>Early for a reason</h2>
<p>Since the calendars arrived, the script also checks whether a timed event before nine (a
flight, an appointment, anything longer than half an hour) sits on today's date. If so, a short
night is not scored as a bad one, and the reason says so. Getting up at five for a plane is not
insomnia, and a spirit that pouts about it would be wrong.</p>

<p>The first score was 67. We chose to take that as encouraging, on the grounds that it was the house's opinion and the house had been up all night too.</p>
<div class="win"><b>The house already has the data for a sleep score; it only needs to be asked.</b>
Read it back from the recorder, name every penalty, and keep the wearable as a witness that
testifies only when it was actually there.</div>
]]></content:encoded>
  </item>
  <item>
    <title>A house that comments on your blood pressure</title>
    <link>https://jan.zorz.si/HA-automations/blood-pressure.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/blood-pressure.html</guid>
    <pubDate>Sat, 19 Sep 2026 16:30:51 +0200</pubDate>
    <description>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…</description>
    <category>health</category>
    <content:encoded><![CDATA[
<p>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.</p>
<p>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.</p>

<h2>A cuff that refuses to be paired</h2>
<p>The monitor will not pair. Ask it and it answers <code>AuthenticationFailed</code>, 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.</p>
<p>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.</p>

<h2>A reference range, not a diagnosis</h2>
<p>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 <em>higher</em>
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.</p>
<p>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.</p>
<div class="note">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.</div>

<h2>The comment, and where it goes</h2>
<p>The comment goes to the phones and to the small display in the living room described on
<a href="https://jan.zorz.si/HA-automations/sparky-screen.html">the screen page</a>: 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
<a href="https://jan.zorz.si/HA-automations/house-spirit.html">house spirit</a> — 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
<a href="https://jan.zorz.si/HA-automations/house-spirit.html">its own page's business</a>.</p>
<p>The automation triggers on the reading's <em>timestamp</em>, 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.</p>
<p>Two bugs are worth telling. The first face we sent was the house's general mood of the moment,
which happened to be <em>curious</em>: 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
<em>measured</em> face for optimal, normal and high-normal, a <em>worried</em> one for any grade of
hypertension or for low. A comment about your arteries should not borrow its expression from the
weather.</p>
<p>The second bug was a small empty square in front of the pulse — a stethoscope emoji, and
<a href="https://jan.zorz.si/HA-automations/sparky-screen.html">the display's fonts have no emoji</a>. 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.</p>

<h2>The readings that did not arrive</h2>
<p>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.</p>
<p>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 <code>No discovery started</code> — 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
<code>device not found</code> 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.</p>

<h2>Charts</h2>
<p>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 — <a href="https://jan.zorz.si/HA-automations/verified-wrong.html">verify the outcome, not
the configuration</a>, as ever.</p>

<h2>The morning nag</h2>
<p>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 <em>nagging</em> face is for,
<a href="https://jan.zorz.si/HA-automations/house-spirit.html">over on its page</a>). At noon, if still nothing, a single message goes
to the phones. The whole thing switches off the moment a reading <em>arrives in the house</em>,
which is not the same as the moment the cuff is used; the button still has to be pressed.</p>
<p>The nightly reviewer that reads the day's logs — <a href="https://jan.zorz.si/HA-automations/machine-colleague.html">our machine
colleague on the night shift</a> — 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.</p>

<h2>The dongle on a stick, and the board by the bed</h2>
<p>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.</p>

<h2>One owner per address</h2>
<p>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.</p>
<p>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.</p>

<h2>A board that reads it itself</h2>
<p>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.</p>
<div class="note">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 <code>Err</code> 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.</div>

<div class="win"><b>Lesson:</b> 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.</div>
]]></content:encoded>
  </item>
  <item>
    <title>A fan with opinions</title>
    <link>https://jan.zorz.si/HA-automations/purifier.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/purifier.html</guid>
    <pubDate>Sat, 12 Sep 2026 20:59:03 +0200</pubDate>
    <description>An air purifier regulated on the trend, floored on the level, allowed to be loud only when nobody is there to hear it. Three small bugs, exported.</description>
    <category>air</category>
    <content:encoded><![CDATA[
<p>An air purifier is a fan with a filter and, these days, an app that would like to know which
continent you live on. Ours disagreed with our answer twice before it accepted the device at
all, after which the device was politely told to speak to the house directly, on the local
network, and the app was left to think whatever apps think. Everything below happens without a
cloud, which in this category is the feature.</p>
<p>A purifier is, in other words, a fan with opinions. The job was to give it better ones than
"Auto", which on this model means "run at a speed you can hear, whenever the sensor has a
feeling about it."</p>

<h2>Off until there is a reason</h2>
<p>The regime is simple: the purifier is off unless something has happened. Three things count.
The <a href="https://jan.zorz.si/HA-automations/detection.html">indoor smoke detector</a> raises its flag. Cooking is detected.
Or the room's own particulate sensor sits above 30 for five minutes, which is the number at
which the air in a small flat has stopped being a rumour. Any of those turns it on, always at
speed two, because "start in the middle and watch" is the whole design.</p>

<h2>Regulate on the trend, floor on the level</h2>
<p>Every five minutes, while it runs, the house compares the room's particulate reading with
the reading from the previous check. Up by three or more: go to speed two. Down by three or
more: drop to one. In between: leave it alone. Speed three exists, and it is a speed you can
hear from the next room, so it is reserved for two situations: the air is genuinely dangerous,
or the room is empty and the air is bad, in which case nobody is there to mind and the fan may
roar. A presence trigger drops it back to two the moment somebody walks in, rather than five
minutes later.</p>
<p>Trend alone had a hole, and the hole was found the way holes are: by asking why the fan was
running at two in a room with pristine air. Because it had been switched on into pristine air,
and pristine air, having nowhere to fall to, produces no downward trend. So there is a floor:
below the "clean" threshold the fan goes to one regardless of what the trend says, and twenty
minutes of clean air with no smoke and no cooking switches it off entirely. That last check runs
on a timer, not on an edge, for the reasons on <a href="https://jan.zorz.si/HA-automations/level-vs-edge.html">this page</a>.</p>
<div class="win"><b>Trend for direction, level for permission.</b> The trend tells you which way
to move. The level tells you whether you are allowed to be loud at all. A fan that only knows
one of the two is either always busy or never bothered.</div>

<h2>Three small bugs, exported for your convenience</h2>
<p><b>The first check.</b> The "previous reading" helper starts life at zero. On its first run
the regulator compared a reading of 4 with a memory of 0, concluded the air had worsened by 4,
and went to speed three in a clean room, loudly. The fix is one clause: no previous reading
means no trend. Every regulator that remembers something must first decide what it believes
before it has remembered anything.</p>
<p><b>The renamed device.</b> Renaming the purifier's entities froze its readings for two hours
while the device stayed perfectly reachable. Frozen numbers are the most dangerous kind, since
they look like calm. There is now a watchdog: four readings unchanged for an hour while the
device answers is not calm, it is a stuck integration, and the integration is reloaded and the
log says so. (<a href="https://jan.zorz.si/HA-automations/traps.html">The traps page</a> has cousins of this one.)</p>
<p><b>The beeper.</b> The device beeps on every command. An automation that changes speed four
times an evening therefore beeps four times an evening, at people trying to watch a film. The
beeper is off. A device that announces every automation is a device that gets unplugged, and
then it announces nothing.</p>

<h2>The one it got wrong, and why we left it</h2>
<p>One afternoon a pot of something steamed for twenty minutes: humidity in the kitchen up ten
points, volatile compounds tripled, particulates flat as a pond. The cooking detector, which
was taught after an earlier mistake to distrust volatiles without particulates, declined to
call it cooking. The smoke detector, which watches a different compound, called it a smoke
session instead. The purifier ran either way, which is the point: it does not care who made the
cloud. The detector's caution stays; the alternative is a purifier that starts whenever somebody
opens a bottle of cleaner.</p>

<h2>The ioniser that was never off</h2>
<p>One of us asked why the ioniser had not been on. Its switch had read <em>off</em> for most of
the week, which looked like a decision. It was not one. The ioniser and the ultraviolet lamp are
settings of the machine, not of the house: they are on whenever the fan is, and they report off
whenever the whole appliance is off, which, thanks to the rules above, is about eighty per cent
of the time. Nothing in the house has ever touched either switch. We flipped the ioniser off and
on with the fan running, to be sure it was a real switch and not a reflection of the power state.
It is. The fan did not blink.</p>
<p>The question underneath was whether to run the machine at its lowest speed round the clock,
for the ions. We declined. An ioniser does not remove particles; it charges them so they settle
on the nearest shelf, which is a redistribution service rather than a cleaning one, with a
whisper of ozone thrown in. The filter does the work. Running the fan all day would spend the
filter five times faster in order to dust the furniture more evenly.</p>

<p>The purifier now runs less, more quietly, and for reasons it could explain if asked. Nobody asks. That is what a good appliance sounds like.</p>
<div class="win"><b>Let the loud setting need an excuse.</b> Give every noisy action a list of
the conditions under which it is allowed, and make "nobody is here to hear it" one of them.
Machines are easy to make effective. Making them polite is the part that takes a week.</div>
]]></content:encoded>
  </item>
  <item>
    <title>The subwoofer, in full</title>
    <link>https://jan.zorz.si/HA-automations/subwoofer.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/subwoofer.html</guid>
    <pubDate>Sat, 19 Sep 2026 15:52:59 +0200</pubDate>
    <description>One Bluetooth link that wants to hear from one caller, a loudness curve drawn by ear, three presets on a schedule, a database for albums mixed without bass…</description>
    <category>audio</category>
    <content:encoded><![CDATA[
<p>The box is an SVS SB-1000 Pro in the living room. It is fed a stereo RCA pair by the same streamer that drives the active monitors, which is the rest of the <a href="https://jan.zorz.si/HA-automations/audio-blob.html">signal path</a>; a Zigbee remote steps the streamer's volume; and the subwoofer itself is controlled over Bluetooth LE by a community integration that exposes twenty-eight knobs, most of which we have now given opinions to.</p>

<h2>A subwoofer that hangs up, politely</h2>
<p>The subwoofer accepts exactly one Bluetooth connection at a time. Its integration therefore does something civilised: sixty seconds after the last command it hangs up, so the manufacturer's phone app can get a word in, and it silently redials whenever the next command arrives.</p>
<p>We learned this by building it wrong. Our first volume automation was guarded by "only when connected", which reads as prudence and works as a bug: the guard blocked precisely the common case, a sub sleeping between commands, even though writing to it is what wakes it. The connection sensor spends most of its life reading "disconnected" while everything is perfectly healthy.</p>
<p>We would like to say we would do it differently next time. We would do it exactly the same, and be exactly as surprised that it works.</p>
<div class="win"><b>Lesson:</b> never condition an automation on a sensor whose "off" is a healthy state. Some connections are meant to flap; guarding on them turns a design feature into an outage. (The field guide gains another entry.)</div>

<h2>Loudness, drawn through three points</h2>
<p>Human hearing loses low frequencies faster than the rest as volume drops, which is why quiet late night listening always sounds thin. The textbook answer is a loudness contour. The household answer: sit on the couch, find the subwoofer level that sounds right at a few volumes, and connect the dots.</p>
<p>It began as two points and a straight line. Then a third point arrived from a quieter evening (volume 20, ear says minus 10) and bent the line where the straight version was one decibel too generous. The curve is now piecewise: <code>sub_dB = -1 - 0.5 x volume</code> up to volume 30, <code>2 - 0.6 x volume</code> above it, continuous at the joint, clamped to the subwoofer's range. The curve lives in a template sensor of its own, so anything that wants to know what the bass should be can simply read it.</p>
<p>An automation holds the bass to that curve whenever the streamer's volume moves. The remote's volume steps arrive in bursts of three, so it waits out the burst and writes once; the subwoofer does not enjoy being dialled like a rotary phone.</p>
<div class="note">Fletcher and Munson mapped this in 1933 with headphones and a generation of patient test subjects, and their contours do exactly what our third point did: they change slope as the level drops. We reproduced the useful part with three data points and two evenings. The curve is not science; it is a preference, made repeatable, drifting toward science one calibration at a time.</div>
<p>A footnote two days later: after some hours of use the driver loosened up and the whole curve moved down by one decibel, measured the only way it can be, by the same ear at the same evening volume. A loudness curve is not a constant of the room; it is a constant of the room, the box and the month.</p>

<h2>Three personalities and a scheduler</h2>
<p>The subwoofer stores presets, so it now has three characters. <b>Music</b> is the reference: the ear-tuned crossover, phase and nothing else, because for music the correct amount of subwoofer is the amount you cannot point at.</p>
<p><b>Movie</b> adds a single decibel and a gentle 3 dB bump around 30 Hz, since film soundtracks carry an effects channel mixed with headroom that living rooms never use. <b>Night</b> is the diplomatic one: eight decibels down and a room gain cut that removes precisely the deep bass that travels through concrete to the neighbours, while the couch keeps most of the illusion.</p>
<p>An automation switches between them: television sound on the eARC path means Movie, anything else means Music, ten in the evening or the house-wide sleep flag means Night. A manual pick from the dashboard is respected for six hours against the source switches; the ten o'clock bell and the sleep flag override it regardless, and eight in the morning hands control back to the sources. It is the convention the light switches in this flat already follow: the human is always allowed to overrule the script, and the script is always allowed to forget about it by morning.</p>
<p>The trap worth exporting: the subwoofer reports which preset is active <em>only in the moment one is loaded</em>, and its preset selector reads "unknown" the rest of the time. The first version of the volume logic keyed off that selector and quietly applied an offset of zero, whatever the preset.</p>
<p>The fix is a rule as old as SCADA: <b>keep your own source of truth</b>. A helper in Home Assistant declares which character the subwoofer is supposed to have; the device's selector is demoted to an actuator; every other automation reads the helper. The subwoofer is consulted about reality only when it volunteers it.</p>

<h2>The ninety degree ear</h2>
<p>The subwoofer's phase control ended up at 90 degrees because that is where it sounded best, which invited the obvious question: is that a real setting or a superstition? The arithmetic says real. At a 60 Hz crossover the wavelength is about 5.7 metres, so 90 degrees is a quarter of that, roughly a metre and a half of effective path difference, which is comfortably what you get from speaker placement plus the few milliseconds a DSP subwoofer spends thinking. At the crossover the sub and the mains play the same notes; misalign them and they subtract.</p>
<p>The ear found the alignment first and the physics signed off afterwards, which is the correct order for a living room and the reverse of how it usually goes in this house. For the sceptical there is a classic verification: invert the sub's polarity, sweep the phase for the <em>deepest</em> null at the listening position, then flip polarity back. The deepest hole, inverted, is the tallest sum.</p>

<h2>Some albums were mixed without a subwoofer in the room</h2>
<p>A loudness curve assumes the recording did its part. Some recordings did not. A drummer's 1990 debut album needed a good deal more sub than the same drummer's 1994 record at the same volume, and no curve drawn through the room can know which year the mastering engineer was having.</p>
<p>It took us three tries to store this sensibly. First we reached for release-year tiers, which is astrology with a discography. Then we reached for absolute targets per album, which lasted until we remembered that the sub sits anywhere from -6 to -40 dB depending on the master volume. An absolute target is a preference frozen at one evening's loudness. On reflection the only sane thing to store is an <b>offset</b>: a number added on top of the curve, so the curve keeps handling the volume and the album keeps its opinion.</p>
<p>The database is a small JSON file keyed artist, then album, then track, most specific wins, and an artist with no entry gets zero. The drummer currently reads: everything of his +3, <em>Master Plan</em> +8, <em>Hard-Wired</em> +5, the last of which began life at +3 and was revised by the button, which is what a living preference looks like. Adding an album is not data entry. Set the bass by ear while it plays, press "remember this album", and a script stores the current sub level minus the curve value. The offset takes effect at once and survives every volume change afterwards, which is the whole point.</p>
<p>A second automation watches the streamer, looks the current album up when a track starts and parks the offset in a helper. It never touches the subwoofer. That restraint turned out to matter.</p>

<h2>The single-writer rule</h2>
<p>Exactly one automation writes the subwoofer's level. Everything else, the curve sensor, the album offset, the preset shift, the fans in the room, only produces a number, and the one writer adds the numbers up, clamps the result and sends it. When a new influence turns up it becomes one more term in the curve sensor, never a second writer. The sanctioned exception is a human thumb on the knob, which the writer politely overrules at the next volume change unless "remember this album" has turned the thumb's opinion into a term.</p>
<p>The reason is partly the radio, one BLE link, sixty seconds of patience, no appetite for being argued with by two scripts at once, and partly hygiene: when the bass is wrong there is one place to look, and the answer is always a sum. The writer now triggers on the curve sensor itself, so a new term does not even need a new trigger. It just appears in the total, like a service charge.</p>

<h2>The air purifier, and the half decibel that was not there</h2>
<p>The <a href="https://jan.zorz.si/HA-automations/ac-loop.html">air conditioner</a> had already earned two decibels on a simple argument: its fan is a steady hiss on the bottom octave, and the ear, which is very good at subtracting steady noise, subtracts a little of the bass along with it. The <a href="https://jan.zorz.si/HA-automations/purifier.html">air purifier</a> joined on the same precedent: from its second speed upwards, one decibel more. This took four lines and should have been the end of the story.</p>
<p>It was not, because the curve rounds, and it rounded the base and the terms together. On the quiet half of the curve the base ends in .5 at every odd volume, and the template engine rounds halves to the nearest even number, so an even term added before rounding lands where it always did while an odd one lands on whichever side parity prefers. The purifier was worth 0, 1 or 2 dB depending on the volume; at one particular volume it was worth nothing at all, which we found by measuring rather than by trusting the configuration, a habit this house acquired the expensive way.</p>
<div class="win"><b>Lesson:</b> round the base first, then add the integer terms. Nobody had noticed because every term before was even.</div>

<h2>A knob, because sliders are for people who do not care</h2>
<p>Fine bass adjustment wants a big round knob, and no dashboard card we could find would draw one for a plain number entity; the knobs that exist are all married to thermostats. So we wrote one in an afternoon: plain JavaScript and SVG, a 270 degree arc, tick marks, and a minus and plus for single steps.</p>
<p>The one design decision that matters is that it draws while you turn and writes only when you let go. The loudness automation had already paid for that lesson with its burst-of-three remote; a knob that wrote on every pixel of drag would have re-learned it in about a second. When something else changes the level, the knob follows, unless a thumb is on it, in which case the thumb wins.</p>
<p>The subwoofer, for its part, has no idea any of this exists. It receives one number every now and then, from one automation and, occasionally, one thumb, and is asleep the rest of the time. Of everything in this flat, it may have the healthiest relationship with automation.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Turning a monitor off is harder than it sounds</title>
    <link>https://jan.zorz.si/HA-automations/monitor-presence.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/monitor-presence.html</guid>
    <pubDate>Sat, 19 Sep 2026 16:30:51 +0200</pubDate>
    <description>A 49-inch screen that warms the office like a second occupant, a Mac that has to be asked to stop sending pictures, and a log that tells the truth a few…</description>
    <category>comfort</category>
    <content:encoded><![CDATA[
<p>The home office has a 49-inch monitor. It hangs off a Mac mini over DisplayPort and nothing else, and when it is on it does two things: it shows a picture, and it heats the room. In September the second of these stopped being a feature. We wanted it asleep whenever nobody was in the office, which sounds like the kind of request a house full of automations should be able to grant before lunch.</p>
<p>It took a day and a metering plug, which for this house is about average.</p>

<h2>Do not talk to the monitor</h2>
<p>The obvious move is to talk to the monitor. DDC/CI over DisplayPort can indeed tell a screen to switch off. What it is much worse at is telling it to switch back on, because a sleeping monitor tends to put its own DDC circuit to sleep along with everything else, and a device that cannot hear you is difficult to persuade. So we talk to the Mac instead.</p>
<p>Over SSH, with a key kept for this one purpose, <code>pmset displaysleepnow</code> stops the picture; the monitor notices the silence and drops into its own power-saving standby, DPMS, which is the monitor's side of the handshake. <code>caffeinate -u -t 1</code> pretends to be a user for one second, the picture returns, and the monitor wakes because it was never really asked to do anything. It works with any monitor, needs no DDC and no USB, and its one fragility is that Remote Login has to stay switched on in the Mac's sharing settings, which we have written down twice, in capitals.</p>

<h2>What it was costing</h2>
<p>We borrowed a metering plug for an afternoon rather than guess. Awake, the monitor draws 84.3 W. In standby, 1.2 W, at a power factor of 0.07, which is the electrical equivalent of a shrug, for the reasons on <a href="https://jan.zorz.si/HA-automations/derived-signals.html">derived signals</a>. The difference is 83.1 W, about 1.7 cents an hour at our 0.20 EUR/kWh, and roughly the heat output of a seated person. So an office with one of us and a lit monitor is, thermally, an office with two of us. That was the whole complaint.</p>
<p>Our earlier estimate of "under half a watt" for standby was optimistic by a factor of two and a bit. We keep saying we will stop estimating things we can measure. The plug is on loan to the office, not a fixture, and nothing permanent is built on it.</p>

<h2>Who decides the room is empty</h2>
<p>The office has an mmWave radar with three detection tiers: large motion to 3 m, small motion to 3 m, and static presence, meaning breathing, to 2 m. It is mounted directly above the monitor, so a person at the desk sits well inside the 2 m static band. Four days of history contained zero cases of "no presence while the Mac was active", which is the number we wanted before letting it switch off anything in anyone's face. Wake is on the <em>edge</em> of presence, the radar going from off to on, so the screen is lit as you walk in, before you reach the keyboard. The reasoning is on <a href="https://jan.zorz.si/HA-automations/level-vs-edge.html">level versus edge</a>.</p>
<p>Sleep is more cautious. The radar has its own fading time of 300 s before it will admit a room is empty; the automation then waits a further two minutes on top. We started with five, which added up to about ten minutes of a lit screen in an empty room; two brings the total to about seven. And even then the Mac must report itself idle, the camera must not be in use and the audio output must not be in use, because a radar will eventually lose a person who has stopped moving altogether, as <a href="https://jan.zorz.si/HA-automations/motion-is-not-presence.html">the living room has already demonstrated</a>, and losing you mid-call would be the wrong way to save 1.7 cents.</p>
<p>The idle guard could in principle block sleep forever, so we checked: the Mac's activity sensor was off for 88.9 of 96 hours. It does not stand in the way.</p>
<div class="note">We did not shorten the radar's fading time, though it would have been the easy win. Seven other automations hang on the same sensor, among them the office light, the fan, the alarm's assessment of the flat and the sleep detector. Trimming one delay would have quietly retuned all of them. The same sensor, from the other side, is on <a href="https://jan.zorz.si/HA-automations/motion-is-not-presence.html">motion is not presence</a>.</div>

<h2>Knowing whether it worked</h2>
<p>Switching the monitor off turned out to be the easy half. Knowing that it was off was harder. The Mac is Apple Silicon, and the display-power object every forum answer says to query, <code>IODisplayWrangler</code>, does not exist there; asking <code>pmset</code> for its power state returns "Internal failure", which is at least honest. The companion app on the Mac offers a "Screen Off" attribute, and we watched it stay False through a real sleep from start to finish. It was measuring something, presumably; not this.</p>
<p>The only source that told the truth was <code>pmset -g log</code>, which records "Display is turned off" and "Display is turned on" with timestamps, a few seconds after the fact. So a sensor built on a shell command reads the last such line over SSH every 60 s and becomes <b>Display - pisarna</b>. When SSH fails, the sensor goes <em>unavailable</em> rather than a confident, fictional "off", in the spirit of <a href="https://jan.zorz.si/HA-automations/fail-visible.html">failing visibly</a>. The <a href="https://jan.zorz.si/HA-automations/machine-colleague.html">first night's review</a> caught the obvious gap: a failed SSH returns no text at all, and a template asked to read the last line of nothing falls over. The sensor now checks that it has a string before reading one.</p>
<p>We could have had the sensor mirror our own commands, "we sent sleep, therefore it sleeps", and it would have looked perfect on the dashboard forever. See <a href="https://jan.zorz.si/HA-automations/verified-wrong.html">verified wrong</a> for how that ends.</p>

<h2>The test that kept failing</h2>
<p>The first three tests were discouraging. The log showed the display turned off and turned on again within the same second, every time, which looked very much like a monitor bouncing back awake and a plan that had not survived contact with hardware.</p>
<p>The same log keeps a list of "assertions", the reasons macOS thinks it should stay awake, and in each case there was one: a HID event, which is to say keyboard-or-mouse input, from the wireless USB receiver. Which is to say, one of us was sitting at the desk running the test and, being a person, moved the mouse. The Mac did exactly what it should do when someone moves the mouse. With hands kept off everything, the screen went dark and stayed dark for 28 s with no "turned on" in sight, and the wake command lit it at 17:07:55, confirmed by the only sensor we fully trust, someone looking at it.</p>
<p>The office tab now has a bar chart of hours per day the screen was on. The history database keeps a week and the sensor is a day old, so for now the chart is mostly a promise. It will fill in.</p>

<div class="win"><b>Before you shorten a delay, find out whose delay it is.</b> The easy win was sitting in the radar's own settings, and seven other automations were sitting on it too. The rest is the usual: measure the sensor rather than your intentions, and accept that a person at the keyboard will always win, as they should.</div>
]]></content:encoded>
  </item>
  <item>
    <title>The bulb that kept applying for a job</title>
    <link>https://jan.zorz.si/HA-automations/matter-ghost.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/matter-ghost.html</guid>
    <pubDate>Sat, 19 Sep 2026 16:30:51 +0200</pubDate>
    <description>A phone in the flat began insisting there was a new Matter device to add, nobody had bought one, and the culprit turned out to be a lamp we already owned.</description>
    <category>field notes</category>
    <content:encoded><![CDATA[
<p>One afternoon a phone in the flat began, politely and repeatedly, to suggest that a new Matter device was nearby and would we like to add it. Nobody had bought anything. Nothing had arrived in the post. Every lamp, plug and sensor we own was already accounted for, working, and in most cases on. The phone was undeterred. It had seen something, and it wanted us to know.</p>
<p>The one thing that had happened was small enough to overlook: the living-room lamp that is the body of <a href="https://jan.zorz.si/HA-automations/house-spirit.html">the house spirit</a>, an IKEA KAJPLATS bulb, had been switched off and on again. Once. That is the whole crime scene.</p>

<h2>The suspect, and the alibi</h2>
<p>A KAJPLATS is a dual-protocol bulb. It speaks Matter over Thread or Zigbee, one at a time, with one radio, and it decides which by counting power cycles. We had deliberately moved ours to Zigbee weeks earlier, by the ritual of power cycles recorded on <a href="https://jan.zorz.si/HA-automations/sparky-screen.html">the lamp's own page</a>: six cycles for a factory reset, ten seconds on to leave Thread, then exactly twelve for Zigbee pairing. The bulb was on Zigbee, and had been the whole time.</p>
<p>What we had not appreciated is that moving to Zigbee does not make the Matter half go away. It leaves it uncommissioned, which is a very different thing. An uncommissioned Matter device believes it is new, and a new Matter device advertises the fact over Bluetooth LE for about fifteen minutes after every power-on. The advertisement carries a small block of service data on the Matter UUID <code>FFF6</code>: an opcode, a discriminator, a vendor id and a product id. The phone's setup service listens for exactly this, and does what it was built to do. It was only being helpful.</p>
<p>The airwaves are full of other helpful things, which is what makes this a whodunit rather than a lookup. Two Bluetooth scans from the home hub, and an mDNS sweep for good measure, found plenty of traffic: manufacturer data from Google's Nearby and Cast services, Fast Pair beacons, a general hum of things wanting to be noticed. None of it was Matter, because by the time we looked, the fifteen-minute window had shut. The vendor and product ids can only be read while the bulb is talking, and it had stopped.</p>

<h2>The catcher</h2>
<p>So we wrote a small one. Twenty-five lines of Python around a BLE scanner: listen for anything carrying <code>FFF6</code> service data, and if the opcode is zero and the packet is long enough, unpack the discriminator, the vendor id and the product id and write them down with a timestamp. Then start it, wait, and have one of us switch the lamp off and on again. Once. Not six times, for reasons that will become clear.</p>
<p>The first run caught nothing, and said nothing about it. The script had been started detached from a directory the hub's container cannot see — the scripts folder under the configuration directory is one it can — so it never actually started. There was no error, only silence, which is the least informative kind of failure. Moving the script and running it again fixed it. This is a lesson we have learned before and will apparently keep learning.</p>
<p>The second run caught it live at 17:03. Opcode 0, vendor <code>0x117C</code>, product <code>0x9005</code>, and close enough to be in the room. <code>0x117C</code> is IKEA. The only IKEA device that had just been powered on was the living-room lamp. Case closed, with the least dramatic culprit imaginable: a bulb that, every time it wakes, spends a quarter of an hour applying for a job it already has.</p>

<h2>What "add" would have done</h2>
<p>Nobody was allowed to tap the friendly button, and the reason is worth the paragraph. The bulb has one radio and runs one protocol at a time. Commissioning it into Matter would take it off the Zigbee network entirely. The house spirit, the <a href="https://jan.zorz.si/HA-automations/arbitration.html">queue of automations</a> that compete for that lamp's colour, everything that reaches it through Zigbee would go dark, and none of it would say so, because as far as Zigbee is concerned a device that leaves simply stops answering. Getting it back means the full ritual again: six, ten seconds, twelve.</p>
<p>There is a second trap on the way to the first. The obvious way to silence a nagging bulb is to turn it off and on until it stops. A KAJPLATS counts power cycles; six of them is a factory reset. One cycle is safe. More than one is a decision, and the prompt on the phone is to be dismissed rather than obeyed.</p>
<div class="note">The first theory was that we had bought a bulb whose firmware had moved on without us: IKEA quietly revised four of its Matter devices this summer, new model numbers and a new radio chip. It was the wrong tree. Those four are battery-powered, the bulb is not among them, and our IKEA fleet, which is <a href="https://jan.zorz.si/HA-automations/hardware.html">most of the mesh</a>, is the old revision anyway.</div>

<h2>Name tags for the real ones</h2>
<p>The lasting fix is not on the bulb but on the map. The Matter server's view of the Thread network shows each node by its <em>NodeLabel</em>, an attribute stored on the device itself (path <code>0/40/5</code>, thirty-two bytes at most), not by whatever name we gave it in the home hub. Renaming a device in the hub does not write it. So for every real Matter node in the flat we set the label by hand and read it back, and repeat that for each new one after its christening.</p>
<p>The effect is that the map now reads like a staff list. Every legitimate node has a name; anything that turns up nameless is a ghost by definition.</p>

<div class="win"><b>A device moved off one protocol has not forgotten the other.</b> The bulb was working perfectly and still believed it was new. When a phone offers to add something you did not buy, find out who is advertising before you accept; the catcher stays in the scripts folder.</div>
]]></content:encoded>
  </item>
  <item>
    <title>Airing into a wall</title>
    <link>https://jan.zorz.si/HA-automations/airing-into-a-wall.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/airing-into-a-wall.html</guid>
    <pubDate>Sat, 19 Sep 2026 16:30:51 +0200</pubDate>
    <description>Two small automations about doors, windows, and the questions nobody thought to ask: is this door actually airing anything, and who is still home.</description>
    <category>ventilation</category>
    <content:encoded><![CDATA[
<p>The bedroom opens onto a balcony, and the balcony is glazed. There is an inner door, which tilts or
swings wide, and beyond it a sliding window that is the actual boundary between us and the weather.
Open the inner door with the outer one shut and you have not aired anything. You have created a
slightly larger, slightly colder bedroom.</p>
<p>The sliding window had been shut against rain a few days earlier. One evening in September somebody
tilted the inner door to air the room and went off to do something else, secure in the knowledge that
ventilation was in progress. It was not. The room got the cold off the glass and none of the fresh
air.</p>

<h2>A door that opens onto nothing</h2>
<p>The house already knew how to check that airing had happened: the <a href="https://jan.zorz.si/HA-automations/weather-air.html">air
advice</a> closes its own loop by watching two contacts on opposite sides of the flat, both open, and
says well done instead of nagging again. What nothing covered was a door opened on a person's own
initiative, into a glazed balcony, where the second contact is not on the far side of the flat but two
metres away.</p>
<p>The fix is a template binary sensor, which we called <em>blind airing</em>. It is true when the inner
door is tilted or wide open and the sliding window is shut, and false otherwise. That is the whole
sensor. It is the same trick as the <a href="https://jan.zorz.si/HA-automations/detection.html">through-draught sensor</a>, a state
neither contact knows on its own, with the sign reversed: that one notices two windows open, this one
notices one door open and the wrong second thing shut.</p>
<p>The sensor is a level fact, true for as long as the condition holds, whatever order you got there
in. It does not care whether you opened the door with the window already shut, or shut the window with
the door already open, which was the rainy-day order of events. The automation still watches the
<a href="https://jan.zorz.si/HA-automations/level-vs-edge.html">moment</a> it becomes true, because that is when a reminder is useful; the
difference is that the fact underneath survives a restart and the moment would not.</p>
<p>Two minutes after that moment, a reminder: the bedroom door is open, tilted or wide, the sliding
window is shut, no air is coming, open the other one too. At the half-hour mark, if it is still true, a
second one. Not while anyone is asleep, though. The consequence of a stuffy bedroom is CO₂, and CO₂ has
its own automation, and waking somebody to tell them they are not ventilating would cost more than the
ventilation is worth.</p>

<h2>The rain exception</h2>
<p>The refinement arrived the same day. When it rains, somebody goes out onto the balcony to fetch
tools, and in the rain a shut sliding window is not a mistake, it is the point. A door open for a few
minutes in those conditions is a person, not an oversight, and a reminder at two minutes is a nag.</p>
<p>So the delay depends on the weather: ten minutes if it is raining, two if it is not. "Raining" is
decided by any one of three sources, because each has been known to be wrong on its own: the weather
provider's millimetres per hour, our own rain nowcast reporting less than a minute until rain, or the
national forecaster's current state being any of the wet ones. One vote is enough.
<a href="https://jan.zorz.si/HA-automations/redundancy.html">Two sources, and their disagreement</a> is the house rule; here the
disagreement is cheap to settle, because the cost of believing a false "raining" is eight extra minutes
of silence.</p>
<p>A template sensor's own delay, the <code>delay_on</code> that would normally do this job, is a fixed
value; it cannot be a template. A state trigger's <code>for</code> can. So the sensor has no delay at
all, deliberately, and the rain-dependent wait lives in the automation's trigger, where it is computed
at the moment the door opens. The comment in the sensor says as much, for the benefit of whoever next
wonders where the debounce went.</p>
<div class="note">The other wrinkle is older, and <a href="https://jan.zorz.si/HA-automations/traps.html">we have paid for it before</a>: a
template entity flickers to <em>unavailable</em> and back on every reload, and a trigger with only a
<code>to:</code> counts the flicker as a real event. So the two-minute trigger says
<code>from: 'off'</code>. The half-hour repeat leans on its own long <code>for:</code> instead, which is
not the same thing and is on the list.</div>

<h2>Leaving, with the other of us still home</h2>
<p>The second story is shorter and more embarrassing. Just before one o'clock the next afternoon, a
phone received the message "Balcony wide open, and you are leaving". The living-room balcony door was
indeed wide open, not tilted, which is the summer setting and stays silent. One of us was indeed
walking away from the flat. The message was accurate in every respect but the one that mattered: the
other of us was at home.</p>
<p>The house knew this, too, and had gone to some trouble to know it. (The
<a href="https://jan.zorz.si/HA-automations/arriving-leaving.html">presence page</a> makes the case for the distinction between an empty
flat and one occupied by somebody else. This automation had simply never been told about it.) That
phone has no home-automation app on it. Its presence is assembled from two things nobody designed for
the purpose: a classic Bluetooth name request, which the phone answers whenever its radio is on, and a
ping from the router, tried every minute, with four consecutive misses required before either dares to
say "away". Two trackers feed one person entity, and both had been saying <em>home</em> all morning.</p>
<p>The automation had never asked. Its one condition was that guest mode, the switch we flip when
somebody is staying with us and the house should stop having opinions, be off. The branch for walking
away checked that the door was wide open and that the alarm was disarmed, so as not to duplicate the
warning the alarm already gives when it arms, and then it sent the message. Who else might be in the
flat did not come into it. An open balcony door is not unattended if somebody is attending it.</p>
<p>The fix is one condition, and the shape of it matters more than the fact of it. It is a template
asking whether the other person entity is anything other than <em>home</em>, rather than the tidier
built-in test for <code>not_home</code>. The difference is what happens when presence is
<em>unknown</em> or <em>unavailable</em>: the template still warns, the state condition would not. We
chose the template on purpose. A warning we did not need costs a glance at a phone. A warning we did
not get costs a wet living room. The automation is quiet only when the house is sure somebody is
there.</p>

<h2>The one we left alone</h2>
<p>There is a sister automation: a storm approaching, the sliding window open, and somebody leaving. It
has the same trigger and the same blind spot, and we did not touch it. Rain coming onto the balcony is
damage, and the person at home may not know it is coming; they are not watching the forecast, the house
is. Silencing that warning because somebody is home would be fixing the wrong thing. The right fix
there is different wording, "ask the other of us to close it" rather than "come back and close it", and
that is on the list rather than in the file. <a href="https://jan.zorz.si/HA-automations/alert-hygiene.html">Alert hygiene</a> has the
general principle: the cure for a wrong alert is rarely no alert.</p>

<div class="win"><b>Lesson:</b> a departure warning must ask who stays. "You are leaving" is a fact
about one person; "this is unattended" is a fact about everyone, and only the second is worth
interrupting somebody for. The bedroom taught the same thing in miniature: a door that is open is not a
door that is airing, and a sensor that knows the difference is worth two minutes of anyone's time, or
ten in the rain.</div>
]]></content:encoded>
  </item>
  <item>
    <title>Motion is not presence</title>
    <link>https://jan.zorz.si/HA-automations/motion-is-not-presence.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/motion-is-not-presence.html</guid>
    <pubDate>Sat, 19 Sep 2026 16:30:51 +0200</pubDate>
    <description>Two radars, one week, and both opposite failures in the same one: a living room that saw a person who was not there, and then failed to see one who was.</description>
    <category>sensors</category>
    <content:encoded><![CDATA[
<p>A motion sensor answers a simple question: did something move. A presence sensor is sold as answering a harder one: is somebody here. The marketing distance between those two questions is about a metre and a half. The engineering distance, we found out over two evenings in September, is the whole width of the living room.</p>

<p>There are three millimetre-wave radars in the flat. Two of them are the subject of this page; the third is in the bedroom and has its own job on the <a href="https://jan.zorz.si/HA-automations/night-calm.html">night score</a>. Both of ours say <em>presence</em> on the box. In the space of two evenings one of them failed in both possible directions: it saw a person who was not there, and then, once we had corrected for that, stopped seeing a person who was. The other has not put a foot wrong, and the difference between them turned out to be the instructive part.</p>

<h2>The phantom on the couch</h2>
<p>One night the living-room lights were still on after everyone had gone to bed. That in itself is a small thing. What sat behind it was not: the sleep flag, the one that tells the house we have retired, had been off all day, and the wind-down that should have raised it had never started.</p>
<p>The living-room radar had reported presence in an empty room for 41 minutes. Its own state said <code>move</code>, its distance readout said <code>0 m</code>. Both sensitivities, move and presence, were at 10, the maximum, and the range was the full 6 m, which in this flat reaches into the hallway. Something in the room was moving as far as the radar was concerned, and our best suspect is the <a href="https://jan.zorz.si/HA-automations/purifier.html">air purifier</a> two metres away, ioniser on, pushing air about. We have never proved it.</p>
<p>Nothing raised an alarm, because nothing was wrong in any way a machine can see. A presence sensor said <em>on</em>. Everything downstream took it politely at its word: the lights, the sleep detector on the <a href="https://jan.zorz.si/HA-automations/detection.html">detection page</a>, the half-hour "somebody has moved somewhere in the flat" flag that the colour alerts <a href="https://jan.zorz.si/HA-automations/arbitration.html">check before speaking</a>. A lie from a presence sensor is not loud. It is the opposite of loud, which is how it survives.</p>
<p>We lowered both sensitivities to 7 and the range to 4.5 m, and within sixteen seconds the room was declared empty, the wind-down came on, and the evening tidied itself up. We wrote down that <code>distance = 0</code> was the signature of noise, that a real person would appear at a real distance, and went to bed pleased with ourselves.</p>

<h2>The person on the couch</h2>
<p>The next evening one of us lay still on the couch, reading. The radar declared the room empty and the lights went out over the reader, which is a remarkably effective way to report a sensor fault. A presence sensor that cannot see somebody reading on the sofa is a motion sensor with a better vocabulary.</p>
<p>We watched the raw numbers this time. A person lying still produced <code>state = move, distance = 0</code>, which is exactly the phantom's signature. On reflection, the diagnosis of the night before had been wrong in the most useful possible way: distance does not separate a phantom from a person, and we should not have concluded anything from it. We keep a whole page of <a href="https://jan.zorz.si/HA-automations/verified-wrong.html">things we verified wrong</a>, and this one joined it the same evening.</p>
<p>The settings went back up: presence sensitivity 10, move sensitivity 7, range 6 m. We checked the outcome this time rather than the settings screen — twelve samples over two minutes with a still person on the couch, presence held on throughout. Our working hypothesis is that the move sensitivity alone drove the phantom, and that stillness and range are what a lying-down person needs. It is only a hypothesis, because we changed all three at once on the first night and cannot honestly credit the cure to any one of them. The proper test is an empty room at night with move at 7. We are waiting for one.</p>
<div class="win"><b>Lesson:</b> range on a presence radar is not a setting, it is a trade. Turn it down and you lose the still person at the far end of the sofa; turn it up and you acquire the hallway, a draught from the purifier, and a phantom who cancels the whole night routine. No setting removes the imaginary people. The best you can buy is one that also keeps the real ones.</div>

<h2>The other radar</h2>
<p>The office has a different unit, and a more talkative one. Instead of a single presence signal it reports three: big movement, small movement, and a third tier for breathing and fidgeting that reaches 2 m. That third one is the entire reason it appears here. It also has a built-in siren, which we have never used and regard with the suspicion one reserves for a smoke alarm that also makes toast.</p>
<p>It sits directly above the monitor, so the desk chair is well inside the 2 m static range. The still person, the one the living room lost, is this radar's easiest case, because a whole tier of the device exists for nothing else. We have not raised its static range and do not intend to; the living room taught us what a generous range buys.</p>
<p>Which of the two is better has no clean answer. The office one has more settings, finer classes and a dedicated tier for people who sit, which is most people most of the time. If we had to choose a single property, it would be that tier. Sitting still is what humans do in rooms.</p>
<div class="note">The office radar's state also decides whether the computer's screen is on. That story has its own page, <a href="https://jan.zorz.si/HA-automations/monitor-presence.html">monitor presence</a>, and is not retold here.</div>

<h2>What a presence sensor actually gates</h2>
<p>The expensive property of a presence sensor is not that it can be wrong. Every sensor can be wrong. It is that its <em>off</em> is consulted by everything, and consulted silently. Lights, sleep, the flag that says somebody is home when they are not carrying a phone, the office screen: each of them asks "is anyone there", takes the answer and moves on. Eight automations read the office radar alone. When the answer is a phantom they all wait. When it is a false empty they all switch off, sometimes over a person with a book.</p>
<p>There is a quieter trap in the same place, and it is arithmetic. A radar holds its own state for a while after a room empties, and the automation reading it then waits its own delay before acting. Nobody ever adds the two together, and the sum is always longer than either number looks on its own. The <a href="https://jan.zorz.si/HA-automations/monitor-presence.html">monitor page</a> has ours, and what it cost us.</p>
<p>So the rule we came away with is small and slightly humbling. Motion is not presence. Presence is not a person. And <code>distance = 0</code> is not a diagnosis, whatever we said just before midnight on the fifteenth.</p>
<div class="win"><b>The rule:</b> tune a presence radar against the hardest case you have, a still person, not against the easiest one, an empty room. An empty room agrees with every setting.</div>
]]></content:encoded>
  </item>
  <item>
    <title>The night shift is a language model</title>
    <link>https://jan.zorz.si/HA-automations/machine-colleague.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/machine-colleague.html</guid>
    <pubDate>Fri, 14 Aug 2026 12:00:00 +0200</pubDate>
    <description>Field notes on an AI caretaker: what it catches, the three ways it goes wrong, and why its most useful sentence is 'I could not verify this'.</description>
    <category>reporting</category>
    <content:encoded><![CDATA[
<p>Somewhere past four in the morning, a language model reads everything this house did in the
last twenty-four hours (every light, door, alert, air reading, which automations fired and
which pointedly did not) and writes up its opinion. It is diligent, unoffendable, cheaper than
any human with the same reading speed, and wrong in ways that deserve their own field notes,
because they are not the ways people expect.</p>
<p>Every night at twenty to five, a language model reads the day back to us and says what it thinks went wrong. It is not always right. It is, however, always awake, never bored, and has no feelings about being told it was wrong, which makes it the ideal colleague for the night shift and a slightly unnerving one for everyone else.</p>

<h2>What it is genuinely good at</h2>
<p>Volume and absence. No human reads a day of logs; it reads all of them, every night,
without morale. And it notices the dog that did not bark: the automation that should have
fired and did not, the sensor that has been saying the same thing suspiciously long, the
heating that ran for an empty flat. <a href="https://jan.zorz.si/HA-automations/self-reporting.html">The reporting page</a>
covers the mechanics; the short version is that it reviews the evidence against a written set
of house rules and files a report.</p>

<h2>The three ways it goes wrong</h2>
<p>None of them are hallucination, which is what everyone asks about. All of them are evidence
handling.</p>
<ul>
<li><strong>Its rules drift behind reality.</strong> Change an automation's behaviour without
updating the rules block and the reviewer reports correct behaviour as a fault, at length,
with citations. It is not wrong; it is faithfully enforcing last month.</li>
<li><strong>Ambiguous evidence becomes confident conclusions.</strong> An incident log written
by several automations was once described to it as if one author wrote every line. It read a
routine informational line as a critical event and built a tidy little case around it. A
person would have made the same mistake; the person would have sounded unsure.</li>
<li><strong>It inherits your assumptions at full confidence.</strong> Tell it a command was
&ldquo;verified safe&rdquo; and it reasons downstream of that for weeks. The air-conditioner
story on <a href="https://jan.zorz.si/HA-automations/verified-wrong.html">the verified-wrong page</a> sailed through several
nightly reviews untouched, because the reviews trusted the same paragraph I did.</li>
</ul>

<h2>The staff also live in the building</h2>
<p>The colleague is not an abstraction in a distant data centre; the session doing the
investigating runs on the same small box as the house. During one memory crisis the largest
discretionary consumer of memory turned out to be the assistant investigating the memory
crisis, at over a gigabyte. The building inspector was standing on the fire hose. That story,
and the diagnosis it produced anyway, is in
<a href="https://jan.zorz.si/HA-automations/horror-stories.html">the horror stories</a>.</p>
<p>Its diligence also needed a leash. The log collector that feeds the nightly review once fed
on its own errors and filled a quarter of the disk overnight; the fix was verified, and the
loop came back through it three days later.</p>
<p>The house now contains machinery whose sole
purpose is to protect it from the enthusiasm of its own staff: a filter with no template in
it, a rate cap, and a circuit breaker that will disable a runaway automation while explicitly
refusing to touch anything with &ldquo;alarm&rdquo; or &ldquo;leak&rdquo; in its name. Details
in <a href="https://jan.zorz.si/HA-automations/traps.html">the field guide</a>.</p>

<h2>Even the staff roster needs a sensor</h2>
<p>The dashboard has a dropdown choosing which model the session runs on. For a day the
session kept coming back as a different, older model, and three confident diagnoses on the box
were wrong before a measurement settled it: the phone app attaches its own model choice to
each message, outranking every local setting.</p>
<p>The fix was not a fourth layer of
configuration. It is a sensor showing which model <em>answered last</em>, sitting next to the
dropdown that states the wish. The discipline the house applies to its heating now applies to
its help; the full sequence is in
<a href="https://jan.zorz.si/HA-automations/verified-wrong.html">the sequel on the verified-wrong page</a>.</p>

<h2>Rules of engagement</h2>
<ul>
<li>Give it measurements, not summaries. It believes summaries.</li>
<li>Require &ldquo;could not check&rdquo; as an answer, and treat it as a finding: it usually
means the evidence pack is missing a state the rules refer to.</li>
<li>Change the rules in the same edit as the behaviour, or enjoy being prosecuted under last
month's law.</li>
<li>Its work is committed to version control nightly, which is both an audit trail and a
performance review.</li>
<li>Hold it to outcomes. &ldquo;The config now says X&rdquo; is its favourite sentence, and it
means as little from the machine as it does from me.</li>
</ul>

<h2>Postscript: the reviewers got reviewed</h2>
<p>After this page was written, the site itself went in front of a panel: four models from
four different providers, blind review first, then rounds of adversarial cross-examination,
run by a neutral chair. The findings were excellent and are worked into the pages you have
been reading. The episode itself, though, belongs here, because it is this page's argument
performed live.</p>
<p>In blind review, one seat asserted that a platform default was &ldquo;1970-01-01, verified
in the source&rdquo;, in a session that had no access to any source. The actual source, read
later, says today-at-midnight, which is what this site had published. The seat was not lying
so much as compressing: &ldquo;verified&rdquo; was doing the work the evidence could not,
which is the precise failure mode half this site documents, performed inside the machinery
built to catch it.</p>
<p>What saved the record was not any reviewer's reliability. The
cross-examination forced a retraction before ratification, so the published consensus said
&ldquo;disputed, verify before publishing&rdquo; instead of a false correction; the panel
later struck the finding, owned the error in its own record, and wrote the lesson better
than I had: the redundancy must extend to the reviewers' own claims, and it is the hedged
output that makes a wrong reviewer recoverable.</p>
<p>Two further panel errors were the tooling's: a mirroring script silently dropped two pages
and the panel reported them absent from the site; a trimmed diff showed figures deleted that
had merely moved down the page, and the panel reported them removed. Confident narration of
a tool artifact, twice in one session, from a review that was right about nearly everything
else. Instruments lie exactly like sensors do, which this site should perhaps have predicted
would apply to the instruments pointed at it.</p>
<p>And one detail kept for the end. The seat that found the physics error in the motor-wear
material reportedly shares a lineage with the model that wrote it. The same capability,
blinded and set in opposition, caught what it had produced. That is the entire case for
adversarial review in one anecdote, and it is why the nightly reviewer above gets evidence
packs and house rules instead of trust.</p>

<p>Its most useful sentence remains 'I could not verify this'. A colleague who says that unprompted is rarer than one who is right.</p>
<div class="win"><b>A machine colleague multiplies the need for discipline, not the supply of
it.</b> It reads faster than you, writes more than you, and believes itself just as readily.
Every rule on this site about sensors that fail healthy applies, word for word, to the
staff.</div>

<h2>Addendum: the morning after two &ldquo;could not verify&rdquo;s</h2>
<p>The rules of engagement above ask for &ldquo;could not check&rdquo; as an answer and treat it as
a finding about the evidence pack. On 19 September the night shift read the previous day back to us
and used it twice in one report, and the morning went on finding out what a refusal is actually
worth. Both times it turned out to be a finding about something rather worse than a thin pack.</p>

<p>The first thing it could not decide was the floor lamp. While the projector runs, the lamp is
capped at a faint glow — the cap has no page of its own, so briefly: 1 out of 255 while a film is
actually running, 25 while it is paused or somebody is browsing the library. The lamp had been on
from just before the projector started until after midnight, spanning the whole session, and the
reviewer pointed out, correctly, that an on/off log cannot tell 1 from 255. It asked for the
brightness history. There was none.</p>

<p>Not none for that lamp. None for any lamp. Home Assistant no longer records light attributes,
brightness and colour included, to keep the database small — the light integration tells the
recorder to drop them — so the database holds <code>friendly_name</code> for every light in the flat
and nothing else. A lamp dimmed to a whisper leaves exactly the same trace as one at full blast,
which is to say no trace at all. The saved automation traces would have settled it, except they keep
only the last five runs and the evening had been busier than that. We had written a house rule that
no evidence anywhere in the building could ever support, and it took a machine reading the logs at
twenty to five to mention it.</p>

<p>The fix is unglamorous and complete, and it is
<a href="https://jan.zorz.si/HA-automations/derived-signals.html">the same move as every other derived signal here</a>: a template
sensor whose <em>state</em> is the lamp's brightness, 0 when off. States are recorded. History sees
it, the long-term statistics see it, the evidence pack now carries its line, and the house rule
about the cap ends with &ldquo;judge from that, the light itself only logs on/off&rdquo;.</p>

<p>The other refusal was about a thing that did not happen. One of us is nagged every morning until
she measures her blood pressure (<a href="https://jan.zorz.si/HA-automations/house-spirit.html">the house spirit joins in, orange and
impatient</a>), and the noon message had not fired. On a day when a reading arrives before twelve
that is exactly right, and the reading had arrived at 09:40 — but the pack carried neither the
reading nor the same-day flag, so the reviewer declined to call the silence a success or a failure.
It now receives both.</p>

<p>Three of the things the report flagged were us; the rest of its error list was outbound-timeout
noise it correctly refused to call findings. One was our own reload during config work —
<a href="https://jan.zorz.si/HA-automations/traps.html">the trap that makes template entities blink</a> — which the rules already
forgive, and which took a speaker's volume command down with it. A Bluetooth adapter reported
&ldquo;not found&rdquo; for about three minutes, because one of us was physically moving the dongle.
And an availability template for the office display crashed on a <code>None</code> the one time the
Mac declined to answer, a bug written that afternoon and fixed the next morning. A night shift that
files your afternoon's work before you have had coffee is doing its job.</p>

<div class="win"><b>Lesson:</b> <a href="https://jan.zorz.si/HA-automations/self-reporting.html">the same-edit rule</a> — add a rule,
add the state it keys off to the evidence in the same breath — quietly assumes the state exists.
One of these had never existed anywhere in the house. The other had been taken out from under us by
a database diet between releases, without a word. Before writing a rule, check that anything in the
building could ever have witnessed it.</div>
]]></content:encoded>
  </item>
  <item>
    <title>Nobody is watching the watchman</title>
    <link>https://jan.zorz.si/HA-automations/dead-mans-switch.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/dead-mans-switch.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>Every alert path in this house ran inside the house. So when the house fell over, it fell over quietly. The fix is a heartbeat that lives somewhere else and…</description>
    <category>monitoring</category>
    <content:encoded><![CDATA[
<p>Take an inventory of how your system tells you something is wrong. Push notifications
through a companion app. E-mail. A chat bot. Maybe a siren. Every one of them lives inside the patient, an arrangement any hospital would query at the planning stage.</p>
<p>The trouble with a house that reports its own health is that it does so with the same mouth it uses for everything else. When that mouth stops, the silence sounds exactly like good news. For a long time we mistook it for good news. This is the story of teaching a building to have someone outside worry about it.</p>

<p>Now notice what they have in common: <strong>they are all processes inside the thing that
might fail.</strong> You have built a smoke detector that is powered by the fire. The push integration, the SMTP client and the chat bot all run in
the same process as the automation engine. When it stops, they stop, and nothing sends a
final message, because sending is exactly the capability that just died.</p>

<div class="note"><b>How this actually looked:</b> a controller stopped for over four hours
overnight. Leak sensors, alarm, boiler cut-off, all unmonitored. Not one alert, because
every alert path was in the process that had stopped. It was eventually noticed by a human opening the
app out of boredom, which is not a monitoring strategy anyone should recommend.</div>

<h2>Invert the logic</h2>
<p>Stop trying to send a message when things break. Instead, send a message continuously
while things are <em>fine</em>, and let the <strong>absence</strong> of that message be the
alarm. This is an old idea from industrial control, a dead-man's switch, and it is a
better fit for home automation than almost anything else you can do in an afternoon.</p>

<pre><code>every 5 minutes:  GET https://&lt;external-monitor&gt;/&lt;id&gt;

external service: if no ping for (period + grace) → alert me
</code></pre>

<p>The critical property: <strong>at failure time, nothing inside your house has to
work.</strong> No template renders, no integration loads, no network stack in your box
does anything at all. Silence is the signal.</p>

<h2>Make the alert carry a diagnosis</h2>
<p>One heartbeat tells you something is wrong. Several tell you <em>where</em>. Put one on
the automation controller, one on the router, and one at any remote site. Then the pattern
of which went quiet is the diagnosis:</p>

<table>
<tr><th>Controller</th><th>Router</th><th>Remote site</th><th>What it means</th></tr>
<tr><td>silent</td><td>ok</td><td>ok</td><td>The controller itself, process or machine</td></tr>
<tr><td>silent</td><td>silent</td><td>ok</td><td>Router, uplink or power at the main site</td></tr>
<tr><td>ok</td><td>ok</td><td>silent</td><td>Power or connectivity at the remote site</td></tr>
<tr><td colspan="3">all silent</td><td>The monitoring service itself, or something very wide</td></tr>
</table>

<div class="note"><b>Keep the checks independent.</b> It is tempting to make the router's
heartbeat also report "can I reach the controller?". Don't. That couples them: a
controller-only fault turns both red and destroys the table above. Each heartbeat should
answer exactly one question, <em>am I alive?</em></div>

<h2>Two failure modes in one channel</h2>
<p>Most monitoring services accept a "fail" variant of the ping URL. That gives you a second
state for free: <em>alive but unwell</em>. Route a short list of internal health signals to
it, but keep the list short. A false alarm at 3 a.m. destroys trust in the channel faster
than a missed one.</p>

<p>The most valuable entry on that list is your own monitoring layer. If the thing that
watches your sensors freezes, that fact should escalate <em>out of the box</em>, rather than
dying quietly alongside everything else.</p>

<h2>Details that decide whether it works</h2>
<ul>
<li><strong>The alert channel must not route back through your system.</strong> If the
monitoring service notifies you via your own chat bot, you have rebuilt the circular
dependency you were removing.</li>
<li><strong>Grace longer than a normal restart.</strong> Otherwise every update pages you,
and you will mute it. Measure how long your restarts actually take, then double it.</li>
<li><strong>The repair must beat the page.</strong> If something self-heals within ten
minutes, do not alert at eight. Alert at thirty, and only about faults that self-healing
could not fix.</li>
<li><strong>The ping URL is a credential.</strong> Anyone holding it can forge a heartbeat
and silence the alarm. Keep it out of version control.</li>
<li><strong>Test it by deliberately failing.</strong> An untested alarm path is
indistinguishable from a broken one, and both look exactly like a quiet week. Batch that testing into one window, scattered tests
look exactly like a flapping system.</li>
</ul>

<h2>On the remote end</h2>
<p>Cheap consumer routers often can't persist a cron entry across a reboot. If the device
has no boot hook, have your controller re-assert the schedule periodically, the same way
you would re-assert any other remote state it forgets. Then accept the residual: if the
remote device reboots while your controller is down, nothing re-arms it. That fails toward
a false alarm rather than a missed one, which is the right direction.</p>

<h2>The paperwork has to live off-site too</h2>
<p>A dead-man's switch tells you the house is down. What happens next depends on paperwork:
the restore runbook, and the one piece of configuration no backup contains, the host's own
network setup, without which a freshly restored machine boots onto the wrong network and
cannot reach its backups. For a while, the runbook describing how to rebuild this machine
lived on this machine. It was well written, kept current, and stored in the one place
guaranteed to be unavailable in the scenario it was written for. Filed, so to speak, inside
the patient.</p>
<p>Wherever your heartbeat monitor lives, put a copy of the recovery notes next to it. The
alarm and the instructions belong on the same side of the disaster.</p>

<p>There is a school of thought that says a system this self-aware is a sign of anxiety. We prefer to think of it as the building finally getting a next of kin.</p>
<div class="win"><b>Cost:</b> one free monitoring account, one scheduled request, about
thirty minutes. It is the highest return-on-effort item in this entire collection.</div>

<h2>The watchman cried wolf</h2>
<p>In mid-September, five weeks after the heartbeat went in, <a href="https://jan.zorz.si/HA-automations/machine-colleague.html">the language model that
reviews the house overnight</a> filed a worrying finding: ten timeouts in one day against the
external monitoring service, and a suggestion that the safeguard might be quietly failing. That is
precisely the sentence everything above was written to make impossible, so we took it seriously and
went to look.</p>
<p>The log was indeed full of cries. Twenty-three timeouts across four days, ten of them on the day
that prompted the finding, every one an <code>ERROR</code>, and all but one stamped twenty-five or
twenty-six seconds after a five-minute boundary — the exception, a minute past the boundary, was a
second attempt. That detail is the whole story. The heartbeat makes up to three attempts, ten
seconds apart, with a twenty-five-second limit on each, so a timeout twenty-five seconds into a
cycle is attempt one giving up. What the log never said, because nothing was writing it down, was
what happened next. We counted. Only once in four days did two attempts fail in the same cycle, and
not one cycle lost all three. The retry had rescued every check-in in those four days, and the
safeguard had been working the entire time.</p>
<p>Measured live, the path was healthy: name lookup instant, handshake a tenth of a second, eight
probes out of eight returned. About two percent of first attempts stall, the same occasional hiccup
our e-mail, chat bot and parcel tracker all see on the way out of the flat. That is exactly why
<a href="https://jan.zorz.si/HA-automations/ten-readers.html">the ten-reader review</a> put a retry here in the first place; before it
existed, slow evenings dropped pings outright, and since it went in the service on the other end has
noticed nothing at all. So we left the limit where it was. It had already gone from ten seconds to
twenty-five in August, and anything that takes longer than that to fetch a few bytes is a stuck
connection rather than a slow one; a longer wait would only delay the retry that fixes it.</p>

<h2>Log the cycle, not the attempt</h2>
<p>The fault was not in the ping. It was that a whole failed cycle, the one event that would
actually matter, had nowhere to show up. Each attempt reported its own small failure and the cycle
as a whole reported nothing, which is like a hospital charting every cough and never the diagnosis.
<a href="https://jan.zorz.si/HA-automations/fail-visible.html">The rule that auditors need the same discipline as alarms</a> turns out
to have a mirror image: an auditor will also cry wolf at noise nobody ever meant it to count.</p>
<p>So the heartbeat now stamps a timestamp helper on every <em>successful</em> check-in, and a
sensor counts the minutes since: under five on an ordinary day, past fifteen and the monitoring
service is already in its grace period with nothing to show for us. A cycle that loses all three
attempts writes one greppable word to the log — the marker reads <code>DEADMAN-CIKEL-PADEL</code>,
"dead-man cycle failed", because the night shift greps in the house's own language. That word, or
the sensor past fifteen, is now the only thing it may report. The bullet above says the repair must
beat the page; this is the same rule pointed inwards, where the repair must also beat the log
entry.</p>
<div class="win"><b>A retry that succeeded is not an incident.</b> If the only thing a retry loop
writes down is its failures, it will eventually be accused of the very thing it prevents: a noisy
success reads much like the silent failure that started this page. Log the outcome of the cycle; the
attempts are weather.</div>
]]></content:encoded>
  </item>
  <item>
    <title>Arriving, leaving, and who else is home</title>
    <link>https://jan.zorz.si/HA-automations/arriving-leaving.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/arriving-leaving.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>Who is home, who is arriving, who just left, and the small tragedies that happen when the house guesses wrong about any of them.</description>
    <category>presence</category>
    <content:encoded><![CDATA[
<p>The transitions (arriving, leaving, someone else being in the place) are where a home
automation system earns most of its keep. They are also where it most often gets things
embarrassingly wrong, because location is noisy and intent is invisible. Nothing announces &ldquo;smart home&rdquo; quite like being shrieked at by your own alarm for the crime of arriving at it.</p>
<p>Knowing who is home sounds like the easy part. It is the part where a phone in a coat pocket, a router's lease table and a radar in the living room all offer slightly different answers, and the house has to pick one before somebody walks into a dark corridor.</p>

<h2>Pre-cool before you get there, using drive time</h2>
<p>Geofences are a poor trigger for anything that takes time to work. By the time you cross a
boundary, you are already at the door. A <strong>live drive-time estimate</strong> is much
better: when the estimate drops below half an hour and the room is genuinely warm, start
cooling.</p>
<div class="note"><b>The guard that makes it work:</b> drive time alone is not enough. Parked
twenty-five minutes from home for the afternoon looks identical to being twenty-five minutes
out and driving. Require the direction of travel to be <em>towards</em> home as well, or you will cool an empty
flat every time you have a long lunch across town.</div>
<p>The general shape is worth stealing for anything with lead time, heating, hot water,
warming a car: <em>estimated time of arrival, plus evidence of actual approach, plus the thing
being worth doing at all.</em></p>

<h2>Do not let the alarm greet you at the door</h2>
<p>Automatic disarming on arrival either delights you or terrifies you, depending entirely on
how it is gated, and you will find out which on a day you are not at home.</p>
<ul>
<li>Trigger on <strong>entering a small zone</strong>, a hundred metres or so, rather than on
a distant approach. A one-kilometre trigger leaves an unprotected gap you did not choose.</li>
<li>Only act if the system is actually armed in an away mode. Never touch it in other states.</li>
<li>Modern phone location is accurate enough for this; the failure mode to design against is
not accuracy, it is a stale location update.</li>
</ul>

<h2>Leaving: make it unconditional</h2>
<p>Departure automations tend to accumulate conditions until they quietly stop firing. The
things that should happen when the last person leaves (air conditioning off, standby power cut
to appliances that do not need it) are better written as <strong>unconditional</strong>
actions, because a command that is already satisfied is harmless while a missed one costs money
for days.</p>
<div class="note"><b>Verify the command is idempotent first.</b> If an appliance's power command
is a toggle rather than a discrete on/off, an unconditional &ldquo;off&rdquo; will switch it
<em>on</em> when it is already off. Check the code table before automating a departure action
for an empty home.</div>

<h2>One-way doors</h2>
<p>A common bug worth naming: an automation that cuts power and disables an integration when you
leave, with the restore path attached only to <em>one</em> of the ways of coming back. Flip the
holiday switch on and off without ever arming the alarm, and the appliance stays disconnected
indefinitely with nothing reporting it.</p>
<p>Every &ldquo;leaving&rdquo; action needs a return path from <strong>every</strong> state that
can mean you are back, and the restore should be unconditional, not gated on the world still
being exactly as it was when you left.</p>

<h2>Inferring that somebody else is home</h2>
<p>A nice piece of reasoning that needs no extra hardware. If motion appears in the home long
after your phone left the area, it is not you. That single inference unlocks a lot: alerts can
be phrased differently, heating can behave differently, and the place stops treating
&ldquo;empty&rdquo; and &ldquo;occupied by someone else&rdquo; as the same state.</p>
<div class="note"><b>Do not reuse a smoothed presence sensor for this.</b> A motion helper with
a long off-delay is designed to <em>hide</em> the exact timing you now depend on. The delay that
makes it good for lighting makes it useless here, use the raw sensors.</div>

<h2>Fewer observations, better presence</h2>
<p>The home-presence estimate here is Bayesian, and its changelog is mostly a list of inputs
removed. Lights went first, then the power plugs, then per-room motion. Every extra signal
looks like free evidence and is in fact another opportunity to be confidently wrong at
3&nbsp;a.m.: a lamp on a schedule is not a person, and a television drawing standby power is
not company. Three observations survived the cull, and the estimate has been calmer ever
since.</p>
<p>The skill is not collecting evidence. It is declining most of it.</p>

<h2>Simulated occupancy that is not obviously fake</h2>
<p>Randomised lighting while away is an old idea, and the details separate
&ldquo;someone is in&rdquo; from &ldquo;this house is running a cron job&rdquo;: weight the rooms by how much they are really used, follow sunset rather than the
clock, and vary both timing and duration. A living room that lights up at exactly 20:00 every
evening advertises an empty home more effectively than darkness does.</p>
<p>The worst outcome is not a wrong guess. It is a confident wrong guess, acted upon. So the house is allowed to be unsure, and told what to do when it is.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Talking to an AEG washer-dryer without the cloud</title>
    <link>https://jan.zorz.si/HA-automations/aeg-local-control.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/aeg-local-control.html</guid>
    <pubDate>Sat, 19 Sep 2026 15:52:59 +0200</pubDate>
    <description>The replication guide: redirect the machine at the router, answer it with a hundred lines of Python, read the wire format, use the field map, and know…</description>
    <category>protocols</category>
    <content:encoded><![CDATA[<div class="note"><b>Two files travel with this page:</b> the guide itself as raw Markdown, <a href="https://jan.zorz.si/HA-automations/aeg-local-control.md">aeg-local-control.md</a>, for people who would rather copy than read, and the sink, <a href="https://jan.zorz.si/HA-automations/aeg_wash_sink.py">aeg_wash_sink.py</a>, published as-is from a working laundry room. The story of how any of this became necessary is on <a href="https://jan.zorz.si/HA-automations/washer.html">the washer page</a>.</div>
<p>This is the replication guide. The story of how we got here, including the two walls that turned out not to be there, is at <a href="https://jan.zorz.si/HA-automations/washer.html">jan.zorz.si/HA-automations/washer.html</a>; this page is only the part you can type.</p>
<p>Where things stand, September 2026: <strong>reading works in full.</strong> The machine connects to a box in the flat, tells it everything it would have told Amazon, and we understand the half of it that matters: state, phase, time, door, programme and every setting on the panel. <strong>Commands are unproven.</strong> The machine appears to accept orders only while a programme is running (seen once, in the cloud's own history), and every test so far was sent to an idle drum, which ignored us with great dignity. Ours is an AEG 9000-series washer-dryer. AEG is an Electrolux brand, so the dialect is probably shared, but the tags, bits and enums below are what one machine told us over three sessions; yours will overlap and differ, and the method survives where the numbers do not.</p>
<h2>What you need</h2>
<table>
<thead>
<tr>
<th>Thing</th>
<th>Why</th>
</tr>
</thead>
<tbody>
<tr>
<td>A router that can NAT by source MAC (ours: RouterOS 6)</td>
<td>The machine's IP is dynamic; its MAC is not</td>
</tr>
<tr>
<td>A Linux box on the LAN with Python 3</td>
<td>The fake broker lives here</td>
</tr>
<tr>
<td>The machine's MAC address</td>
<td>From your router's DHCP leases</td>
</tr>
<tr>
<td>A self-signed certificate on an ECDSA P-256 key</td>
<td>RSA gets "no shared cipher"; the machine offers elliptic-curve suites only</td>
</tr>
<tr>
<td>An empty drum</td>
<td>You will be pressing buttons on the panel. Do not press Start</td>
</tr>
</tbody>
</table>
<p>Two facts make this possible, and both are the manufacturer's choice, not ours: the machine never verifies the server certificate, and it sends no MQTT username or password. It proves who it is with a client certificate and then takes your word for who you are.</p>
<p>So the certificate is the only thing you need to make, and it takes two commands:</p>
<pre><code class="language-sh">openssl ecparam -name prime256v1 -genkey -noout -out local_ec.key
openssl req -new -x509 -days 3650 -key local_ec.key -out local_ec.crt \
    -subj &quot;/CN=&lt;the broker hostname your machine resolves; ours was the maker's mqtt-ecc… host&gt;&quot;
</code></pre>
<p>We only ever tested it with the broker's real name, so use that. Whether the machine checks the common name is untested, and this is not the place to find out.</p>
<h2>Step 1: redirect</h2>
<p>The machine opens TCP 8883 to the maker's broker. Two NAT rules turn that into TCP 8899 on your box. Both carry a comment so they can be removed in one go. Arm the timer first; the rules come second.</p>
<pre><code>/system scheduler add name=AEG-LOCAL-revert start-time=([/system clock get time] + 5h) interval=0 \
    on-event=&quot;/ip firewall nat remove [find comment=AEG-LOCAL]; /system scheduler remove AEG-LOCAL-revert&quot;
/ip firewall nat add chain=dstnat protocol=tcp src-mac-address=&lt;MACHINE-MAC&gt; dst-port=8883 \
    action=dst-nat to-addresses=&lt;BOX-LAN-ADDRESS&gt; to-ports=8899 comment=AEG-LOCAL place-before=0
/ip firewall nat add chain=srcnat protocol=tcp dst-address=&lt;BOX-LAN-ADDRESS&gt; dst-port=8899 \
    action=masquerade comment=AEG-LOCAL place-before=0
</code></pre>
<p>The timer lives on the router, not the box, because the box is the thing you will be restarting and editing all afternoon. <code>place-before=0</code> puts a rule on top; on an empty table omit it. Source-MAC matching needs the router to see the machine's own frames; if an access point or ISP box does NAT in between, match on <code>src-address</code> with a static lease instead. The box must accept TCP 8899 on its own firewall, and if your forward chain drops LAN-to-LAN by default, allow this one.</p>
<p>The second rule is the one we forgot the first time. On one subnet the box answers the machine directly, the router never gets to undo its rewrite, and the machine discards a reply from a stranger; the masquerade sends it back the long way. The story page has the evening this cost.</p>
<p>The rules affect new connections only. The machine's existing session to Amazon carries on until it drops, so expect the first knock on your door within a few minutes; to hurry it, kill the tracked flow:</p>
<pre><code>/ip firewall connection remove [find src-address=&lt;MACHINE-IP&gt; dst-port=8883]
</code></pre>
<p>Rollback is the mirror image. Remove the rules, then stop the sink, because an established connection stays translated until it closes and the machine will otherwise keep talking to you, quite happily, for hours:</p>
<pre><code>/ip firewall nat remove [find comment=AEG-LOCAL]
/ip firewall connection remove [find dst-port=8899]
</code></pre>
<p>Once the socket is gone the machine dials home within seconds. A running wash never notices either way, because the drum does not consult the network.</p>
<h2>Step 2: the sink</h2>
<p>You need something on port 8899 that speaks MQTT 3.1.1 over TLS and says yes. A real broker would do; we wrote a couple of hundred lines of Python instead, because a broker that writes down everything it hears is the point, and configuring mosquitto to accept a client with no password and an unchecked certificate is more lines than writing one. The script is published next to this guide as <code>aeg_wash_sink.py</code>; run it as <code>AEG_SESS=&lt;session dir&gt; python3 aeg_wash_sink.py 8899</code>, with the certificate files where the top of the script expects them.</p>
<p>The TLS side is three settings:</p>
<pre><code class="language-python">ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(&quot;local_ec.crt&quot;, &quot;local_ec.key&quot;)
ctx.verify_mode = ssl.CERT_NONE
ctx.set_ciphers(&quot;ALL:@SECLEVEL=0&quot;)
conn = ctx.wrap_socket(sock, server_side=True)
</code></pre>
<p>An RSA certificate may sit alongside; it will never be picked. The MQTT side is a handful of replies. Anything else can be logged and ignored.</p>
<table>
<thead>
<tr>
<th>Machine sends</th>
<th>You answer</th>
<th>Bytes</th>
</tr>
</thead>
<tbody>
<tr>
<td>CONNECT</td>
<td>CONNACK, accepted</td>
<td><code>20 02 00 00</code></td>
</tr>
<tr>
<td>SUBSCRIBE (packet id, topic, qos)</td>
<td>SUBACK, granted</td>
<td><code>90 03 &lt;id-hi&gt; &lt;id-lo&gt; &lt;granted-qos&gt;</code></td>
</tr>
<tr>
<td>PUBLISH</td>
<td>nothing (ours: qos 0); if qos 1, PUBACK</td>
<td><code>40 02 &lt;id-hi&gt; &lt;id-lo&gt;</code></td>
</tr>
<tr>
<td>UNSUBSCRIBE</td>
<td>UNSUBACK</td>
<td><code>B0 02 &lt;id-hi&gt; &lt;id-lo&gt;</code></td>
</tr>
<tr>
<td>PINGREQ</td>
<td>PINGRESP</td>
<td><code>D0 00</code></td>
</tr>
</tbody>
</table>
<p>The machine reconnects about every seventeen seconds until something answers, which makes every mistake free: fix the code, wait, it knocks again. Once connected it does two things. It subscribes to one command topic, <code>cmd/&lt;PNC&gt;/00/&lt;SERIAL&gt;/&lt;MAC&gt;</code>, which is the channel it waits on for orders. And it publishes its state to another, <code>$aws/rules/appliances_events/evt/&lt;PNC&gt;/00/&lt;SERIAL&gt;/&lt;MAC&gt;</code>. The sink keeps the subscribed topic that starts with <code>cmd/</code>; that is where commands go later.</p>
<p>Did it work? On the router, <code>/ip firewall connection print where dst-port=8899</code> shows a flow if the redirect is doing its job. In the sink log, <code>TLS ok (TLSv1.2 ECDHE-ECDSA-AES128-GCM-SHA256)</code> means you are done. If instead the sink says <code>NO_SHARED_CIPHER</code>, your certificate is RSA. If the handshake never completes and the machine's Wi-Fi icon dies within a minute, the masquerade is missing. If nothing at all happens in five minutes, the MAC is wrong, an earlier dstnat rule is catching the packet first, or the old Amazon session is still alive; see the connection-remove line above.</p>
<p>Our sink keeps five files per session, and we recommend all five:</p>
<table>
<thead>
<tr>
<th>File</th>
<th>What</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>raw.log</code></td>
<td>every MQTT packet in hex, so a decoder bug loses nothing</td>
</tr>
<tr>
<td><code>frames.jsonl</code></td>
<td>one decoded frame per line; tail this for anything live</td>
</tr>
<tr>
<td><code>live.log</code></td>
<td>readable, with value changes highlighted; this is the file you watch</td>
</tr>
<tr>
<td><code>params.json</code></td>
<td>last value of every parameter and how often it moved; written every 20 frames and on disconnect</td>
</tr>
<tr>
<td><code>cmd_queue.txt</code></td>
<td>write a hex frame on a line, the sink publishes it to the command topic</td>
</tr>
</tbody>
</table>
<h2>Step 3: wire format</h2>
<p>Each MQTT payload is a thin JSON envelope around hex:</p>
<pre><code class="language-json">{&quot;version&quot;:&quot;1.0.0&quot;,&quot;data&quot;:[{&quot;payload&quot;:&quot;&lt;HEX&gt;&quot;,&quot;timestamp&quot;:&lt;ms since epoch&gt;}]}
</code></pre>
<p>Each payload is one frame:</p>
<pre><code>AD | module (6 ASCII) | container (1) | field (1) | type 0x06 | value (rest)
</code></pre>
<p>Values are ASCII or big-endian integers, except when they are nested TLVs: <code>64 | (tag, length, value)*</code>. Most of the interesting settings live in TLVs, and a TLV read as a decimal integer looks like a random ten-digit number that changes by one, which fooled us for a moment. Read hex.</p>
<p>A worked example, the temperature turned from 40 to 30 °C on the panel:</p>
<pre><code>AD 525031574431 1C 09 06 64  02 01 28  04 01 20  06 06 000100000000  11 01 00  0B 01 00  32 02 0000  0C 01 00  0A 02 FFFF  13 01 40
AD 525031574431 1C 09 06 64  02 01 1E  04 01 20  06 06 000100000000  11 01 00  0B 01 00  32 02 0000  0C 01 00  0A 02 FFFF  13 01 40
</code></pre>
<p>Module <code>RP1WD1</code>, container <code>1C</code>, field <code>09</code>, then a TLV. Tag <code>02</code> is the temperature, in degrees, exactly: <code>0x28</code> is 40, <code>0x1E</code> is 30. Nothing else moved. That is the whole method: change one thing on the panel, diff the frame, name the tag.</p>
<h2>Step 4: the field map</h2>
<p>The machine sorts its fields by meaning. Container <code>04</code> is the device and its clocks; container <code>1C</code> is the programme. This held for every field we decoded and saved us a great deal of guessing.</p>
<p>Container <code>04</code>, device and time (module <code>RP1WD1</code>):</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Meaning</th>
<th>Values</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>01</code></td>
<td>appliance state</td>
<td>0 off, 2 running, 3 ready, 6 end of cycle</td>
</tr>
<tr>
<td><code>02</code></td>
<td>UI/menu state</td>
<td>1 running, 2/3 flip on menu visits; use <code>01</code> instead</td>
</tr>
<tr>
<td><code>11</code></td>
<td>time to end</td>
<td>seconds; <code>FFFFFFFF</code> means no data</td>
</tr>
<tr>
<td><code>12</code></td>
<td>delay start</td>
<td>seconds; <code>FFFFFFFF</code> means not set</td>
</tr>
<tr>
<td><code>22</code></td>
<td>language index</td>
<td>21 Slovenian, 0 English on ours</td>
</tr>
<tr>
<td><code>29</code></td>
<td>display brightness</td>
<td>1 to 5</td>
</tr>
<tr>
<td><code>2A</code></td>
<td>end-of-cycle sound</td>
<td>0 off, 1 short, 2 medium, 3 long</td>
</tr>
<tr>
<td><code>60</code></td>
<td>door</td>
<td>1 open, 0 closed</td>
</tr>
</tbody>
</table>
<p>Container <code>1C</code>, programme:</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>09</code></td>
<td>selection TLV: what is chosen now</td>
</tr>
<tr>
<td><code>0A</code></td>
<td>limits TLV: what this programme allows</td>
</tr>
<tr>
<td><code>10</code></td>
<td>cycle phase: 2 wash, 3 rinse, 4 spin, 6 dry, 9 drain</td>
</tr>
<tr>
<td><code>20</code></td>
<td>soft-plus TLV, tag <code>01</code>, 0 or 1 (a permanent setting)</td>
</tr>
<tr>
<td><code>22</code></td>
<td>permanent extra rinse, 0 or 1</td>
</tr>
</tbody>
</table>
<p>Tags inside the selection TLV (<code>1C 09</code>):</p>
<table>
<thead>
<tr>
<th>Tag</th>
<th>Meaning</th>
<th>Encoding</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>02</code></td>
<td>temperature</td>
<td>°C</td>
</tr>
<tr>
<td><code>04</code></td>
<td>spin</td>
<td>rpm / 50 (<code>0x20</code> = 1600)</td>
</tr>
<tr>
<td><code>06</code></td>
<td>options</td>
<td>6-byte bitfield, bits below</td>
</tr>
<tr>
<td><code>0B</code></td>
<td>extra rinse count</td>
<td>1, 2, 3 (with bit 12 set)</td>
</tr>
<tr>
<td><code>13</code></td>
<td>time save</td>
<td>enum: <code>0x40</code> off, <code>0x20</code> quick, <code>0x04</code> extra quick</td>
</tr>
<tr>
<td><code>0C</code></td>
<td>dryness</td>
<td>enum: <code>0x01</code> timed, <code>0x08</code> iron-dry, <code>0x20</code> cupboard, <code>0x80</code> extra</td>
</tr>
<tr>
<td><code>0A</code></td>
<td>drying time</td>
<td>minutes (only with dryness <code>0x01</code>)</td>
</tr>
<tr>
<td><code>32</code></td>
<td>programme code</td>
<td>2 bytes; our dial has 17 positions, one of them Settings</td>
</tr>
</tbody>
</table>
<p>Bits in tag <code>06</code>:</p>
<table>
<thead>
<tr>
<th>Bit</th>
<th>Option</th>
</tr>
</thead>
<tbody>
<tr>
<td>7</td>
<td>prewash</td>
</tr>
<tr>
<td>10</td>
<td>stains</td>
</tr>
<tr>
<td>11</td>
<td>soak</td>
</tr>
<tr>
<td>12</td>
<td>extra rinse on</td>
</tr>
<tr>
<td>16</td>
<td>anti-crease</td>
</tr>
<tr>
<td>17</td>
<td>steam</td>
</tr>
<tr>
<td>29 / 30 / 31</td>
<td>steam level low / mid / max (steam programme only, exactly one set)</td>
</tr>
<tr>
<td>32</td>
<td>wash mode</td>
</tr>
<tr>
<td>33</td>
<td>dry mode</td>
</tr>
</tbody>
</table>
<p>Tags inside the limits TLV (<code>1C 0A</code>):</p>
<table>
<thead>
<tr>
<th>Tag</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>14</code> / <code>15</code></td>
<td>temperature max / min</td>
</tr>
<tr>
<td><code>16</code> / <code>17</code></td>
<td>spin max / min (rpm / 50)</td>
</tr>
<tr>
<td><code>06</code></td>
<td>supported options, same bit layout</td>
</tr>
<tr>
<td><code>13</code></td>
<td>allowed time-save values</td>
</tr>
<tr>
<td><code>0C</code></td>
<td>allowed dryness values</td>
</tr>
</tbody>
</table>
<p>The rule that turned out to matter more than any single tag: <strong>never guess a value set, read the mask.</strong> Limits tag X lists exactly the values selection tag X may take. We guessed the dryness levels as 0x20/0x40/0x80; the mask said 0x01/0x08/0x20/0x80, and the mask was right. Wool has a lower spin ceiling than cotton, dry-only sets every wash limit to <code>FF</code>, and the machine itself clamps the selection when you change programme. It tells you what it will accept before you ask.</p>
<h2>Step 5: commands</h2>
<p>The machine subscribed to a command topic in step 2. A command is, as far as we can tell, the same frame shape with the changed tag inside the selection TLV, wrapped in the same JSON envelope, published to that topic. Write the hex line into <code>cmd_queue.txt</code>; the sink does the rest.</p>
<p>The catch is permission. In the cloud entity, before the redirect, the machine's remote-control flag read <em>not safety relevant enabled</em> whenever the drum was idle, and in that state it drops every command without a reply. We saw it flip to <em>enabled</em> by itself when a cycle started and back off at the end, once, in the cloud history of the 15 September wash. We have not found the local field for that flag; locally, use cycle phase (<code>1C 10</code> not zero) as the proxy and treat the window as unverified.</p>
<p>The test, then: start a spin-only or rinse-only programme with an empty drum from the panel, never remotely; wait for <code>1C 10</code> to move; queue one frame with only tag <code>04</code> changed. Do not use the panel's delay-plus-start trick to enable remote start on an idle machine; that arms it for remote START, which is the one command never to send.</p>
<p>If the next wash still ignores the frame, the suspects are the leading byte (<code>AD</code> may mean reporting, with a different byte for requesting) and the envelope.</p>
<h2>Step 6: Home Assistant</h2>
<p>We have not written this yet; today HA sees the washer only through its power plug. The plan is a small daemon that tails <code>frames.jsonl</code>, translates the fields above, and publishes MQTT Discovery entities to your own broker. It is a hundred lines the reader can type as fast as we can, and the first of them looks like this:</p>
<pre><code class="language-json">homeassistant/sensor/washer_time_to_end/config
{&quot;name&quot;:&quot;Washer time to end&quot;,&quot;state_topic&quot;:&quot;washer/time_to_end&quot;,
 &quot;unit_of_measurement&quot;:&quot;s&quot;,&quot;device_class&quot;:&quot;duration&quot;,&quot;unique_id&quot;:&quot;washer_time_to_end&quot;}
</code></pre>
<p>Then publish the value of <code>04 11</code> to <code>washer/time_to_end</code> whenever it changes. Time to end, phase, door, state and the chosen programme are enough for a dashboard and every automation we have wanted.</p>
<p>Two consequences follow and neither is a bug. While the redirect is active the vendor app is blind: one connection, one owner, and it cannot be proxied because the leg to the cloud is mutual TLS with a key that never leaves the machine. Going local for good, with a permanent NAT rule or a DNS rewrite, kills the app permanently. That is a choice. We have not made it yet, because the app still has the twelve programmes that are not on the dial.</p>
<h2>Safety, and a confession</h2>
<ul>
<li>Arm the auto-revert before the rules. A router rule with no timer is a laundry room with no cloud, forever, at three in the morning.</li>
<li>Do not publish your MAC, LAN addresses, client ID or topic strings; the last two contain the serial. Every placeholder above was real once.</li>
<li>Never send START. Empty drum, one tag at a time, spin speed first.</li>
</ul>]]></content:encoded>
  </item>
  <item>
    <title>Listening to a washing machine</title>
    <link>https://jan.zorz.si/HA-automations/washer.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/washer.html</guid>
    <pubDate>Sun, 06 Sep 2026 12:00:00 +0200</pubDate>
    <description>One full cycle captured at the router: what the appliance says to its cloud, how it authenticates, and why the first local-redirect test failed for the…</description>
    <category>protocols</category>
    <content:encoded><![CDATA[
<p>The washer-dryer, an AEG L9WAN61BCE, talks to its maker's cloud and to nothing else. That much
every owner knows. What we did not know, and could not find written anywhere, was <em>how</em> it talks:
which server, which protocol, whether the conversation could be kept inside the flat. So on a
Saturday with a full drum we listened to the whole cycle, three hours and twelve minutes of it,
from the first packet after power-on to the last one after the door unlocked.</p>
<p>The machine that washes the clothes also talks to a server in another country, in a protocol it will not discuss with anyone else. We wanted to know exactly what it says. This required a router with a packet sniffer, a Saturday, a full drum, and a certain amount of humility at the end.</p>

<h2>How to listen without a laptop in the laundry room</h2>
<p>The router can mirror traffic. Its packet sniffer streams frames over UDP to any host, and a
sixty-line Python receiver on the home server turns that stream into a standard capture file.
Filter on the machine's hardware address and the firehose (four megabytes a second for the whole
flat) becomes a trickle: 5,900 frames for an entire wash. The receiver, the router control and
the flow classifier were written and rehearsed weeks earlier on synthetic traffic, because the
machine only connects when somebody is home to start it.</p>
<p>Two things bit us anyway. Frames <em>towards</em> the machine arrived with a VLAN tag, so a
parser that assumed plain Ethernet saw one direction only and confidently reported that DNS
answers never came back (they did; the server answered on the same segment, past the router).
And the router's sniffer stops streaming after about 100 kilobytes while still reporting
<code>running: true</code>; a watchdog that restarts it whenever the capture file stops growing
saved the day, at the cost of a few lost seconds per restart.</p>

<h2>What the machine actually does</h2>
<p>At power-on it asks our own DNS resolver (not a hard-coded public one, which was the first
thing we checked and the best news of the day), resolves three names, and opens exactly two
kinds of connection. Three short HTTPS sessions to a provisioning host on Microsoft Azure in the first minute, then
nothing more from that host all day.</p>
<p>And one long MQTT-over-TLS session to the manufacturer's
message broker, which lives on Amazon's AWS IoT Core, held open for the entire
cycle: about five small records a minute from the machine, a hundred and thirty kilobytes up,
twenty-eight down, a modest burst when the door unlocks. NTP once an hour. That is the whole
vocabulary.</p>
<figure style="margin:1.5em 0">
<img loading="lazy" src="https://jan.zorz.si/HA-automations/washer-flows.png" alt="Timeline of the washer's traffic during one cycle: a burst
of HTTPS at power-on, then a steady MQTT trickle for three hours, a small bump at the end"
style="width:100%;border-radius:12px">
<figcaption>One wash, ten-minute bins. The provisioning burst at the start, then a broker
heartbeat of about seventeen kilobytes every ten minutes, and the end-of-cycle bump.</figcaption>
</figure>

<h2>The wall, precisely measured</h2>
<p>Reassembling the TLS handshakes by sequence number told us what the summary could not. The
broker presents a certificate from AWS IoT Core and then sends a
<em>CertificateRequest</em>; the machine answers with its own certificate and a signature. The
certificate's common name is the appliance's identifier, the same string the cloud integration
uses. So this is mutual TLS with a per-device key that lives inside the machine. Both channels
do it. Nothing is in the clear, nothing is guessable, and there is no local port to speak of.</p>
<p>What follows from that is exact. Pointing the broker's hostname at a local server is trivial,
since the machine trusts our resolver. Getting the machine to <em>accept</em> that server is the
question, because it will check the server's certificate against Amazon's root, and
ours cannot be signed by that.</p>
<p>The test is cheap: a self-signed certificate with the right name,
a probe that requests the client certificate but never rejects it, and the router redirecting the
machine's port 8883 to the probe. The expected result is a TLS alert, "unknown CA", and the end of
the road.</p>

<h2>The test that failed for a different reason</h2>
<p>We ran it, and the machine's display said the Wi-Fi connection had failed. That was not the
alert we were waiting for. The capture showed the machine associating normally, finishing its
HTTPS provisioning, and then sending SYNs to five different broker addresses in turn, with no
reply to any of them. The redirect rule had matched all twenty-one packets. The probe had
accepted zero connections.</p>
<p>The reason is old and well documented, and we walked into it anyway: the machine and the home
server sit on the same subnet. The router rewrote the destination and forwarded the packet to
the server, but the server's reply went straight back to the machine over the same wire, never
passing the router, never being un-rewritten.</p>
<p>The machine received an answer from an address it
had never contacted and discarded it. A destination NAT between two hosts on one segment needs a
source NAT as well, so that the reply is forced back through the box that did the rewriting. We
knew this. We had simply never needed it at home.</p>
<p>Rolling back took one command and the machine reconnected to its real broker within seconds;
the rest of the wash ran on the cloud as usual. The corrected version, with the masquerade and a
probe that no longer closes after a single connection, was staged for the next load. We still
expected the "unknown CA" alert. We were wrong about that in the most useful possible way.</p>

<h2>The wall that was not there</h2>
<p>A washer does not need a wash to phone home; the radio wakes on power-on, cycle or no cycle, so
the next test cost nothing but a press of the button. The connection came back through the redirect
and failed on something we had not expected: not "unknown CA" but "no shared cipher" — a snub
before the introductions. The cause was mundane. This class of device offers only elliptic-curve
ciphers, and we had turned up holding an RSA certificate, the wrong kind of key entirely. One
certificate on the right curve later, and the very next retry — the machine knocks every seventeen
seconds, a free test loop — went straight through.</p>
<p>And here is the punchline. The machine had gone to real trouble to prove who <em>it</em> was: a
per-device certificate, a signature, mutual TLS, the works. Then it accepted our self-signed
certificate for who <em>we</em> were without a second glance, and never once checked it against
Amazon's root. It shows the stranger its passport and never asks to see one back. The wall everyone
assumes behind mutual TLS to a cloud was only ever locked on one side, and not ours.</p>

<h2>What it says once you answer</h2>
<p>So we swapped the dumb probe for a hundred lines of MQTT — a broker that says yes to everything
and writes down the rest — and the machine, with no username and no password to its name, told us
everything in the clear. It subscribes to one command topic, the channel it waits on for orders,
which is precisely the channel one would speak into to give them. It publishes its state to another.
The payloads are a maker's binary format, hex in a thin JSON wrapper: little frames tagged with a
module, a parameter and a value. Firmware, model, the entire menu of programs, the live state of the
drum, parameter by parameter, arriving unencrypted at a broker we wrote in an afternoon.</p>
<p>Two things still stand between this and local control: the binary fields have to be decoded, best
done by watching the numbers move during a real wash, and a command has to be composed rather than
merely overheard, since the machine only ever sent its state and never an order. But the door has no
lock; it simply never occurred to anyone to try the handle from inside the house.</p>

<div class="win"><b>Mutual TLS to a cloud is not a wall until you test it.</b> A device that
authenticates itself with a client certificate looks unassailable, so the reflex is to give up on
local control. But two questions hide in that handshake: does the device prove itself, and does it
check the server back? Here the first was locked and the second wide open, and the only way to learn
that was to hold up a certificate and watch. Listen before you redirect; then redirect before you conclude.</div>

<h2>A whole wash, decoded</h2>
<p>Of the two things still standing between us and local control, the first fell quickly. The next
real wash was three kilograms on the three-hour programme, and the binary gave up its grammar
before the first rinse. A frame names a module, a field and a value; values are either plain
numbers or nested lists of tag, length and value, the oldest trick in the protocol book and none
the worse for it. Eighteen fields were named by the end of the evening, most of them from that one
cycle: the remaining time in seconds, ticking down sixty a minute like a clock that has read the
manual; the cycle phase; and the cycle counter, which went up by one at the end, as cycle counters
should.</p>
<p>Then we turned the programme dial once round on an idle machine and got the complete dial:
seventeen positions, each announcing its two-byte code. The last position is not a programme at all.
It is Settings, which the cloud calls <code>MACHINE_SETTINGS_HIDDEN_TEST</code> and which we had
been discarding as noise for about ten minutes. Six command names came along too (on, off, pause,
resume, start, stop-and-reset), which is the whole vocabulary of orders one can give a drum.</p>
<p>The decisive trick is the one we are slightly embarrassed about. The cloud integration for Home
Assistant (a community one, talking to the maker's API) publishes a diagnostics dump naming every
parameter the machine has, with its current value. We had been staring at a large number in a
counter field wondering what it might be; the cloud was, at the same moment, reporting the same
number under the words "total washing time". Cross-referencing raw values against names resolved
ten fields in one pass without a single guess. The machine had a dictionary the whole time. We had
simply not asked the other half of the system for it.</p>

<h2>The command that was ignored</h2>
<p>With the addresses known, we composed our first order: the frame the machine itself publishes
when the spin speed changes, same module, same container, same field, one byte different, and posted
it to the command topic the machine subscribes to. Nothing. No reply, no change, no complaint. The
machine remained in perfect health and perfect indifference.</p>
<p>The answer arrived days later, and not from the capture. The cloud integration keeps a history of
the machine's remote-control flag, and that history shows the flag flipping to Enabled in the very
second a programme starts running, with nobody near the panel, and back to a state that politely
declines commands the second the cycle ends, on the one wash the cloud was allowed to see, which is
a sample of one and we know it. We had sent our order to an idle machine. It had, by its own rules,
no authority to take it.</p>
<p>The manual's button ritual for enabling remote control (press Delay, then Start) turns out to be
for remote <em>start</em> of an idle machine, which is the one thing we do not want a script to be
able to do. So the frame may well be correct; it was simply posted at the wrong hour. The test now
needs a running drum, which will happen the way our best experiments happen: incidentally, and
after lunch. On reflection this is the safest possible design for a machine that spins at 1,600 rpm.
We would rather it ignored us than the other thing.</p>

<h2>Twenty-one settings in forty-five minutes, no wash</h2>
<p>The remaining unknowns were settings that never move on their own, so they had to be moved by
hand. With the redirect up and the drum empty, one of us stood at the panel and changed exactly one
thing at a time, and the other watched the log for the address that jumped. Brightness, chime,
language, spin, steam, and sixteen more, most of them flipped back afterwards to catch the same
address twice. Nobody pressed Start.</p>
<p>The individual fields are the least of it. Two containers matter for settings: one for the
device and its clocks (brightness, language, remaining time, delay), one for the programme. Within
the programme, one field holds what is selected and its neighbour holds what the programme
<em>allows</em>: maximum temperature, maximum spin, which options exist. Switch from Cottons to Wool
and the limits drop from 95 degrees and 1,600 rpm to 40 and 1,200, and the selection is clamped to
fit, by the machine, before anyone has said a word.</p>
<p>Options are bits, and the machine publishes its own "supported" mask beside them, so there is
nothing to guess. A choice is encoded in four different ways, depending on the mood of whoever wrote
that part of the firmware. Dry-only sets every washing limit to "not applicable", the machine's way
of agreeing that temperature is a strange thing to ask about when there is no water.</p>
<p>We kept a ledger of guesses made before the data arrived, because a model that cannot predict
is only a description. Five hit, one was a hedge and does not count, three missed. The dryness
levels we guessed as a tidy progression, one bit each; the machine used a different one, and its
own mask, which we had in front of us and had not read, said so all along. The rule is short: never
guess which values a field accepts. Read the mask.</p>
<p>Two small delights, both about time. An extra rinse costs nine minutes for the first and four
for each further one, so the first presumably includes a drain and spin the others skip. And
anti-crease adds thirty minutes of gentle tumbling after the cycle that the displayed duration never
counts, so a "finished" notification built on the countdown would arrive half an hour early. The
machine is not lying; it simply has a different opinion about when it is done.</p>
<div class="win"><b>Lesson:</b> when a device publishes what it allows next to what it has, read
the allowance first; the mask was right every time and our arithmetic only most times, which is
the wrong number for a machine that heats water.</div>

<h2>If you want to do this yourself</h2>
<p>Everything needed to repeat this on the same family of machine is on one page,
<a href="https://jan.zorz.si/HA-automations/aeg-local-control.html">the recipe</a>: the redirect (destination NAT plus the masquerade
we forgot), the sink, the wire format, the field map, the commands and the safety rules. The same
text is there as plain <a href="https://jan.zorz.si/HA-automations/aeg-local-control.md">markdown</a>, because recipes should be
copyable. Two honest warnings go with it. While the machine is redirected the maker's app is blind,
because there is one connection and it is talking to you; the wash itself runs regardless. And
whether to make this permanent, with a local broker replacing the cloud for good, is a decision we
have not yet made. The door has no lock. That does not oblige anyone to move in.</p>
]]></content:encoded>
  </item>
  <item>
    <title>The audio-video blob: eight protocols, one couch</title>
    <link>https://jan.zorz.si/HA-automations/audio-blob.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/audio-blob.html</guid>
    <pubDate>Mon, 31 Aug 2026 12:00:00 +0200</pubDate>
    <description>The living room signal path: streaming to subwoofer across eight protocols, an HDMI matrix that retired a seance with a 2010 television, two infrared remotes…</description>
    <category>audio</category>
    <content:encoded><![CDATA[
<p>The living room audio chain was never designed. It accreted, one reasonable decision at a
time, and the result is a signal path that crosses eight protocols between a play button and a
bass note: Spotify Connect in the cloud, a streamer on the LAN, a Zigbee remote, a Bluetooth
subwoofer, an eARC handshake that has changed hands once, a Chromecast and an operator's set-top
box sharing an HDMI matrix, four dialects of infrared, two of them synthesized because the
remotes no longer exist and one learned from a remote that only speaks infrared while its
Bluetooth is asleep, and Matter over Thread to the button that starts an evening and to the
plug that watches whether the projector's lamp agreed to it. None of these devices speak each
other's language. All of them answer to the same couch. The address of this page still says
audio blob, from the days when the blob only listened; the video half arrived later and the
name on the door stayed, the way a town keeps the name of a river that has since moved.</p>
<p>The living room audio path is a monument to the principle that every device may speak only to the device it was designed to speak to, and to nothing else. Eight protocols between a streaming service and a subwoofer. It works, every evening, and nobody who has not seen the diagram would believe why.</p>

<figure style="margin:1.5em 0">
<img loading="lazy" src="https://jan.zorz.si/HA-automations/audio-blob.png" alt="Diagram of the living room audio chain: Spotify feeds the
CXN100 streamer over the cloud, the streamer feeds Kali IN-8 active monitors over balanced
XLR and the subwoofer over a stereo RCA pair, a Chromecast and an operator set-top box feed an
HDMI matrix that drives a 2010 Sony TV and a 2008 Epson projector and hands audio back to the
streamer over eARC, the projector's mains run through a metering smart plug, and Home Assistant
speaks Zigbee to the remote and the screen, LAN, Wi-Fi to the Chromecast, BLE, four dialects of
infrared, Matter over Thread to the plug and a scene button, and the Spotify cloud API as the
control plane" style="width:100%;border-radius:12px">
<figcaption>The blob, drawn to scale of nothing. Solid lines carry signal, wavy lines are
wireless and carry only opinions; the subwoofer gets its music over honest copper and its
orders over Bluetooth. The matrix in the middle arrived in September and took the eARC job
away from the television. The box at the bottom is the only participant that speaks all eight
languages, one of them in four accents, talks to the Chromecast over Wi-Fi in two protocols,
has the Spotify cloud on speed dial as well, which is how a play button on a Zigbee remote ends
up choosing a playlist in another country, and, as of this week, can tell the set-top box to
wake up. The one grey line that is not audio or video is the projector's mains cord, which
passes through a plug that reports current over Thread; it carries no music, only
testimony.</figcaption>
</figure>

<h2>The necromancy of eARC, and its retirement</h2>
<p>The television is from 2010. It predates apps, assistants and most of the acronyms in this
paragraph, and for a year it had exactly one modern trick: it could hand its audio to the
streamer over eARC. The catch took a while to isolate: the audio only routed if <em>the
television itself woke the streamer</em>. If the streamer was already awake when the TV came
on, the handshake completed, every status light agreed, and no sound arrived. The streamer
reports <code>on</code>; only <code>playing</code> means sound.</p>
<p>So the automation did the opposite of what intuition suggests. When the television turned
on, we turned the streamer <em>off</em>, and let the TV resurrect it. One power cycle, executed
at the only moment it helped, and eighteen years of HDMI politics were settled.</p>
<div class="win"><b>Lesson:</b> model the handshake, not the power state. Two devices being
"on" says nothing about whether the negotiation between them succeeded, and the correct
automation sometimes looks exactly like sabotage.</div>
<p>Then a second screen arrived, and with it an HDMI matrix: four inputs, two outputs, and an
eARC port of its own. The Chromecast and the operator's set-top box go in, the television and
the projector come out, and the matrix, not the television, now hands audio to the streamer.
The seance is over. The automation does the boring thing at last: switch the streamer on and
select the eARC input, then keep a watchdog on <code>playing</code> and reselect if a screen
is on and the sound is not.</p>
<div class="win"><b>Lesson:</b> when the topology changes, retire the trick with it. A
workaround that outlives its reason is not a workaround any more; it is a bug with a good
backstory, and the power cycle that once saved the evening would now sabotage it.</div>

<h2>The remote is dead, long live the specification</h2>
<p>The projector is an Epson from 2008. Its remote control is wherever remotes go, and buying
a replacement for a discontinued model felt like defeat. But an IR remote is not magic, it is
a modulated bitstream with a published grammar, and the projector's dialect is plain NEC:
a 9 ms leader, then thirty two bits at 562 microseconds a pulse, address first, command after,
each followed by its complement.</p>
<pre>
leader:  9000 us mark, 4500 us space
bit 0:   562 us mark,  562 us space
bit 1:   562 us mark, 1687 us space
frame:   address 0x5583 (every Epson projector since roughly forever),
         then the command byte and its bitwise complement
</pre>
<p>The command table is public in the crowd-sourced IR databases: power on is 0x90, power off
0x91, and there are twenty five more, direct input selection included, which the buttons on
the projector itself cannot do. We generated the Broadlink packets straight from the timing
grammar, no learning, no original remote, and the very first synthesized packet woke the
projector from across the room. A dashboard tab now carries all twenty seven commands,
including a proper navigation pad, so the projector's menus are once again reachable from a
seated position.</p>
<div class="win"><b>Lesson:</b> a lost remote is an inconvenience. A lost specification would
be a problem. Plastic breaks; protocols are forever, and the databases that catalogue them
are one of the quiet treasures of the hobbyist internet.</div>

<h2>The television learns the same trick</h2>
<p>The matrix cannot switch its outputs by itself; it wants infrared, and it came with a
remote, so its codes were learned the honest way: one button at a time, thirty seconds per
button, and a learning routine that abandons the whole list at the first missed press. Three
buttons per attempt is the rhythm that works. Inputs, outputs and the eARC toggle now live in
the house's vocabulary.</p>
<p>The television's remote, it turned out, had gone the way of the projector's. Same cure. Its
dialect is Sony's, twelve bits at 600 microsecond ticks behind a 2.4 millisecond leader, the
command first and the device address after, sent three times because that is what the
original does. The useful discovery in the tables was that the set has <em>discrete</em> power
codes, one that means on and one that means off, alongside the toggle on the physical remote.
Discrete codes matter more than they sound: a toggle in a dark room is a coin flip, and an
automation that flips coins is not an automation. The input buttons need the fifteen bit
variant, a second device address for the same television. The television is now a switch in
the system: on, wait eight seconds for it to wake, select the input the matrix feeds.</p>
<div class="win"><b>Lesson:</b> prefer discrete codes over toggles wherever a table offers
them, and when a device has two address spaces, the button you want is usually in the other
one.</div>

<h2>Four buttons and an End</h2>
<p>With two sources and two screens the couch got four combinations and one way out: each
source with each screen, and End. A scene wakes the Chromecast if that is the source (it sleeps
readily and does not wake for an HDMI cable), routes the matrix, powers the chosen screen, and
tells the streamer to listen. End puts everything back to sleep and lets the room lighting
resume its opinions.</p>
<p>The projector's power supply is on a metering plug that is never switched off. It speaks
Matter over Thread, the eighth protocol in the room and the only one that carries neither sound
nor picture, merely testimony. What the plug is for is the thirty seconds after the on-code: a lamp from 2008 does not always strike on
the first attempt, and the plug's current is the only witness. Below a threshold, the code is
sent again, up to twice, and the house says so. The screen, incidentally, gained half a second
on its way down; a full second, on reflection, was too much.</p>
<p>Two smaller courtesies round it off. The television follows the Chromecast: it wakes when
the Chromecast wakes, so the Chromecast's own remote is enough to start an evening, and it
switches off a minute after the Chromecast goes to sleep, unless the projector is the screen
in use. And a second two-rocker button joined the first, this one on Matter over Thread rather
than Zigbee, which quietly made the protocol count eight; the diagram said seven for a while
before anyone recounted. One press for the Chromecast on the big screen, a double press for the
set-top box on it, the other rocker for End.</p>

<h2>The set-top box that will not be told</h2>
<p>The operator's set-top box is the one participant that answers to nobody. It lives on the
operator's own router, behind the operator's own address translation, and the operator would
like it to stay there. A spare VLAN on the room switch and a second leg on the automation host
now reach it, which sounds like progress and is not: every port is closed, awake or asleep, no
casting, no remote service, and ping answers identically in both states. Reachable and
controllable turned out to be different words.</p>
<p>Bluetooth, then. The host's own radio can play a keyboard, and it did: the box paired,
encrypted the link, and did so four times in an afternoon. It never once subscribed to a
report, dropped the link after twenty seconds, never came back on its own, and after each
attempt its scanner went blind until the next reboot, which buys exactly one more attempt.
The lesson from the part that did work is exportable: a GATT server that registers itself per
session hands the client a different attribute layout each time, and the client, quite
reasonably, throws away its cache and gives up. Keep the server resident; toggle only the
advertising. A microcontroller that speaks both flavours of Bluetooth is in the post, and this
section will be continued.</p>
<div class="win"><b>Lesson:</b> "reachable" is a network property and "controllable" is a
policy one, and no amount of the first buys you the second. Also: attribute handles are part
of the contract. Move them and you have a new device, whether or not you meant to.</div>
<p><b>Postscript, one day later.</b> It can be told after all, provided you speak the one
language it was not built to hide: infrared. The box's remote is Bluetooth, but pull the box's
plug and the remote, having lost its partner, falls back to infrared like any remote from the
last century, and the box has a receiver waiting for exactly that. Ten seconds without power,
two presses of the power button into the learner, and the house owns a code: Manchester-coded,
no leader pulse, two frames a press, identical every time, which makes it a toggle rather than
an honest on or off. Replayed from a 38&nbsp;kHz blaster into a 36&nbsp;kHz receiver it works
from across the room. It now runs at the start of the Neo scenes, guarded by the only state we
have, namely which input the matrix was on, and the metering plug that will make it honest is
next on the list. The box still answers to nobody on the network. It just turns out to have
ears.</p>

<h2>A subwoofer with its own page</h2>
<p>The subwoofer started as one paragraph here and kept growing until it had more opinions than the rest of the signal path combined. At that point it stopped being a footnote to the living-room signal path and became a story about one Bluetooth link that only ever wants to hear from one caller. It has moved to <a href="https://jan.zorz.si/HA-automations/subwoofer.html">its own page</a>.</p>

<h2>One remote to rule the blob</h2>
<p>Holding the blob together is a small Zigbee remote that was designed to control a
different manufacturer's speakers. Its community blueprint needed a fork: the upstream
volume stepping only applied to grouped players, which is the kind of sentence that costs an
afternoon to be able to write. It now moves volume three steps per press, and its play
button first checks the streamer's input and reroutes it to Spotify when something else has
wandered onto it, because a play button that plays nothing is a lie.</p>
<p>Its two spare gestures went to the house's sense of humour: one dot toggles the streamer's
power, and a double dot asks the <a href="https://jan.zorz.si/HA-automations/house-spirit.html">house spirit</a> to show itself
in colour on the nearest lamps. No protocol in the stack strictly required that last feature.
It stays.</p>

<p>Count the seams: a cloud service, a LAN protocol, Zigbee, Bluetooth LE, eARC and CEC over
HDMI, Google Cast, Matter over Thread, and raw infrared in four dialects, two learned and two
written from the specification. Eight ways for machines to disagree, now with a matrix in the
middle, wired into one system by the only integrator that speaks all eight. Nobody would design
this. It
merely works, every evening, from the first press of a play button to the last note the
neighbours generously tolerate.</p>

<h2>A screen with no bottom</h2>
<p>The projector needed a screen, and the screen got a motor on a Zigbee shutter switch: three
commands, up, down, stop. Its upper limit is honest mechanics, the screen simply cannot roll
any further. Its lower limit switch is broken, so "down" means "down until somebody says
stop", which is not a position a system can rely on.</p>
<p>The switch's automatic calibration, the
one that runs the motor to both ends and times the trip, was therefore out of the question;
it would have unrolled the screen into the floor looking for a switch that no longer exists.</p>
<p>The manual calibration turned that defect into a feature. In manual mode the switch measures
the trip up, then drives down and records "fully closed" wherever the human presses the third
button. We pressed it at the height that fits the picture, a hand's width above the true end.</p>
<p>From then on "closed" <em>means</em> that height: the switch times the trip itself and stops
there, and every position in between is a percentage of a run that ends at the picture, not at
the floor. Our first timing run was spoiled by a well-meant stop command sent during the
measurement, which taught us that a calibration in progress must be left alone; the second run,
done entirely from the phone standing at the screen, took thirty seconds.</p>
<p>The resolution is about a centimetre per percent, so the last few millimetres were settled
with one very short pulse and then frozen as a cinema position at three percent. The screen
follows the projector: on when the projector comes on, up when it goes off, with the buttons on
the audio tab for the times when a human wants it otherwise.</p>
]]></content:encoded>
  </item>
  <item>
    <title>The phone that introduces itself</title>
    <link>https://jan.zorz.si/HA-automations/phone-ip.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/phone-ip.html</guid>
    <pubDate>Sat, 12 Sep 2026 20:59:03 +0200</pubDate>
    <description>A phone that reports its own public address, a firewall list with a two-day memory, and five ways the obvious version did not work.</description>
    <category>access</category>
    <content:encoded><![CDATA[
<p>The house has a front door on the internet, and the door has a list. Addresses on the list
may knock; everyone else gets the sound of nothing. The phone, which is the one thing that
knocks from outside every day, changes its address several times a day, on trains, in cafés,
and most reliably about eight seconds after arriving home. For years the answer was dynamic
DNS, which is a way of telling the door a rumour and hoping the rumour has not expired.</p>
<p>Dynamic DNS is a rumour with a time-to-live. The replacement is a phone that introduces
itself, and a door that forgets the introduction on a schedule.</p>

<h2>The phone measures itself</h2>
<p>An automation app on the phone watches for every change of connectivity: Wi-Fi joined, Wi-Fi
dropped, mobile data back. It waits eight seconds for the dust to settle, asks a public
"what is my address" service, and posts the answer to a webhook on the house. It does this at
boot too, and every six hours regardless, because an app that only speaks when something
changes cannot tell you it has died.</p>
<p>The house checks the answer the way you would check a stranger's ID: it must look like an
address, it must not be a private one, and it must not be from the shared ranges carriers hand
out to a thousand phones at once. If it passes and it differs from the last one, the house asks
the router to put it on the list, with a comment saying who did it and an expiry of two days,
and tells us. Old entries with the same comment are removed. That is the entire mechanism.</p>
<div class="win"><b>Every hole you open should carry its own closing time.</b> The two-day expiry
is not a detail; it is the design. If the house dies, the entry dies with it. A firewall rule
that outlives the thing that justified it is how every network ends up with a rule nobody dares
delete.</div>

<h2>Five ways this did not work first</h2>
<p><b>The source address was right there.</b> The obvious plan was to ignore what the phone
says and read where the request came from. The webhook trigger exposes no headers and no peer
address, and behind a proxy it would have been the proxy anyway. So the phone has to measure
itself, which is less elegant and more true.</p>
<p><b>The JSON that was not.</b> The phone's IPv6 lookup fails on some networks and returns an
error message. The app pasted the message into the JSON body, quotes and all, and the body
stopped being JSON. The house answered <em>200 OK</em> and ran nothing, because the parser
failed before the automation was consulted, and a parser failing before your code is the most
silent failure there is. The body is now form-encoded, a format that a stray quotation mark
cannot break. The <a href="https://jan.zorz.si/HA-automations/money-telemetry.html">wallet feed</a> learned the same lesson a
month earlier, which is embarrassing, and is why it is written down twice.</p>
<p><b>The variable that was a string.</b> A template variable that returns a dictionary returns
a string that looks like a dictionary, and the first attempt to read a field from it read a
character instead. Fields are now read inside one template, which is the boring correct answer
to most template questions.</p>
<p><b>The port.</b> An afternoon went on the wrong one. A later hotel then blocked the
respectable-looking port and let the odd one through, which settled the argument in the odd
one's favour and is the closest thing to a code review a hotel Wi-Fi will ever give you.</p>
<p><b>The shared address.</b> On mobile data the phone's "public" address is carrier-grade NAT:
a thousand strangers wearing the same coat. The list admits the coat; the login still has to
admit the person, which it does, and which is why the list was never the only lock. IPv6 gives
the phone a prefix of its own, and that one is honest.</p>

<h2>The diary</h2>
<p>A side effect nobody designed: the house now keeps a log of when the phone changed its mind
about where it was, with timestamps. It reads like a commute. Home, train, office, café, train,
home, roughly four changes a day, the last one always at the front door, eight seconds after
the key. If the log ever shows a change at three in the morning we will have learned something,
though possibly not about networking.</p>

<p>There is a proper answer to all of this, an overlay network that makes the phone a resident wherever it stands, and it is installed and waiting. The list is what works today, and today, as ever, is where the house lives.</p>
<div class="win"><b>Make the device introduce itself; make the door forgetful.</b> Trust the
introduction only after checking it, admit it for a bounded time, and log every change. The
result is a door that is open exactly as long as it needs to be and can explain, later, why.</div>
]]></content:encoded>
  </item>
  <item>
    <title>Three I have not seen anywhere else</title>
    <link>https://jan.zorz.si/HA-automations/favourites.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/favourites.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>The favourites, judged on the only axis that matters, originality: things we have not seen anywhere else, which is either a compliment or a warning.</description>
    <category>favourites</category>
    <content:encoded><![CDATA[
<p>The first version of this page ranked three favourites on an engineer's axes: proudest, most
inventive, most complex. On reflection, those were the wrong axes. A tunnel between two flats
is a Tuesday, and a heartbeat to an external service is a product with a free tier; solid
work, but solid work is not what a favourites page owes its reader.</p>
<p>Asked which parts of this we are proudest of, we found we had to answer a different question first: proud on what axis? Not the cleverest, since clever is cheap. Not the most useful, since a light switch wins that. The ones that we have not seen anywhere else, and would not have thought of a year ago.</p>
<p>What it owes is the one
axis that cannot be bought: <strong>things I have not seen anywhere else</strong>. Ranked by
that, the list changes, and the former champions move to a shelf at the bottom, which they
keep, because they earned it fairly on the old axes.</p>

<h2>1. The house with a mood</h2>
<p>Everything else here optimises something. <a href="https://jan.zorz.si/HA-automations/house-spirit.html">Sparky</a> optimises
nothing, and it is by our own vote the coolest idea in the house, which for
a feature with no function is quite the career.</p>
<p>What makes it original is not the lamp, it is
the model behind the lamp: a five-dimensional affect vector (valence, arousal, comfort,
sociability, agency) computed from sensors the house already had, plus what is playing, the
music's genre and the film's, resolved live, so a horror night reads differently from a jazz
evening, smoothed so a mood has
inertia, with an episodic memory of when things last happened, and, the load-bearing part,
<strong>explainable on demand</strong>: every dimension shows which inputs drive it and by
how much, so when the mood is wrong the argument is about a number you can see.</p>
<p>Commercial &ldquo;mood lighting&rdquo; maps a sensor to a colour and calls it ambience.
This maps a household's day to a temperament, gives it manners (never touch a lamp in use,
stand down for any alert, go dark for an empty room), and lets you debug its feelings. I have
not found anyone else doing affective state modelling for a dwelling, with explainability, as
a single quiet lamp. It remains the only thing on this site visitors ask about.</p>

<h2>2. Sensing a life nobody bought sensors for</h2>
<p>Not one detector, a family, all raised on the same question: which signal that already
exists changes when this happens? Cooking, from the particulate rise at the nearest sensor. A heated-tobacco session, told apart from cooking by a channel that moves the
<em>opposite</em> way: formaldehyde climbs while particulates stay flat, and the general
trick, when one channel cannot separate two events, find a channel where they disagree in
sign rather than size, is the cleanest single idea this house has produced.</p>
<p>And the third
member logs a strenuous and entirely private bedroom activity, in two countries, by the one
channel no appliance can counterfeit: carbon dioxide, because an air conditioner can move
heat and humidity but cannot breathe. Its ledger is a CSV with a location column and a
discretion policy; the site describes the physics and nothing else.</p>
<p>A four-model review panel recently went through every claim on this site; this family came
back with two cosmetic caveats, and its anchor sentence was the one piece of physics nobody
even tried to dispute. Details on <a href="https://jan.zorz.si/HA-automations/detection.html">detection</a>.</p>

<h2>3. The bank installed a sensor in my wallet</h2>
<p>Card payments as a telemetry stream: captured on independent paths that fail differently,
watched for silence, because a dead parser looks exactly like a frugal week, and, the part I
have genuinely not seen anywhere, treated as <strong>hostile input</strong>: a merchant name
is attacker-controlled text headed for the dashboard of the platform that runs the alarm, so
it is sanitised on the way in and escaped on the way out, both ends.</p>
<p>The same well-read
panel went looking for prior art and reported that the genre does not contain this page. The
build is on <a href="https://jan.zorz.si/HA-automations/money-telemetry.html">money as telemetry</a>.</p>

<h2>The engineer's shelf</h2>
<p>The former champions, demoted with their honours intact.</p>
<p><strong>The three heartbeats and their truth table</strong> remain the highest
return-on-effort item in the house, and yes, the mechanics are
commodity; the page about them never claimed otherwise. Its argument was always about where
the observer stands, not how clever the ping is. The full case, table included, is on
<a href="https://jan.zorz.si/HA-automations/dead-mans-switch.html">the watchman page</a>.</p>

<h2>The best single experiment: mapping the wiring by watching a kettle-sized load</h2>
<p>Six smart plugs in different rooms, each reporting voltage. Individually that number is useless, it wobbles, it disagrees between plugs, and nobody cares what the mains voltage is.</p>
<p>Together they are an instrument. (An instrument, not protection: the safety framing, what a
persistent sag means and when to call an electrician, sits with the method on
<a href="https://jan.zorz.si/HA-automations/derived-signals.html">derived signals</a>.)</p>
<p>First, calibration, because it turned out to be the whole game. At <em>idle</em>, with nothing
drawing power, those six plugs disagreed with each other by <strong>&minus;0.8 to +1.5 volts</strong>.
That spread is measurement tolerance, not wiring, and any threshold applied to raw readings would
have been roughly half false alarms. So each plug now learns its own resting offset against the
median of the others, slowly, and every drop is measured against <em>that plug's own</em> expected
value.</p>
<p>Then the decomposition. Take the median drop across all plugs as the <em>common-mode</em>
component, that is the incoming supply sagging, and it affects everyone. Whatever remains on
an individual plug is <em>differential</em>, that is its own circuit, and it is the part that
indicates a problem.</p>
<p>And then the experiment. Switch on a two-kilowatt heating load and watch all six:</p>
<table>
<tr><th>Plug</th><th>Change</th><th>What it means</th></tr>
<tr><td>one particular appliance</td><td><b>+2.0 V sag</b>, decaying to 0.7</td><td>same circuit as the load</td></tr>
<tr><td>four others, different rooms</td><td>&minus;0.4 V</td><td>other circuits, the supply barely notices</td></tr>
<tr><td>the load's own plug</td><td>&minus;0.1 V</td><td>&hellip;see below</td></tr>
</table>
<p>That is a circuit map of a flat, derived without opening the consumer unit, from hardware bought
to switch lamps on. The decay is the thermostat settling into its duty cycle. And the last row is
the part that would have caused a wrong conclusion if it had been the only measurement: <strong>the
plug feeding the load reports almost no sag of its own.</strong> Electrically it should see the
most, it sits at the end of the loaded branch; that it reports nothing is a measurement artifact
of a coarse, filtered meter describing an event it is part of. (An earlier version of this
paragraph asserted a mechanism here.</p>
<p>A review panel pointed out, correctly, that a passive plug
regulates nothing; the honest statement is that the cause is unconfirmed and the loaded plug's
reading is simply not to be trusted.) The practical rule survives unharmed: measure at the load
and you see nothing; measure at its neighbours and you see everything.</p>

<h2>The plumbing that taught the syllabus</h2>
<p>The tunnel to the remote site is, on this review, ordinary networking with an extraordinary lesson
plan: three separate layers silently discarded configuration (a consumer router that wipes
hand-made rules on any Apply, a container that forgets packet forwarding on every restart,
and a routing UI that accepts entries the kernel never creates), and all three failed the
same way, the stored configuration said yes, the running system said nothing at all.</p>
<p>Whether these are good ideas is for you to judge. That they are unusual, we can promise; we have looked.</p>
<div class="win"><b>The conclusion that generalises.</b> Anything that cannot be made persistent
must be made <em>repeated</em>. A single reconciliation loop now runs every ten minutes and asserts
the whole desired state, the address translation rule, the default route, the specific
return route, the forwarding setting, each check a no-op when it is already correct. It does
not detect breakage and repair it. It simply never assumes anything stayed done.</div>
<p>Which is the same idea as the heartbeat, arrived at from the opposite direction. The heartbeat
assumes nothing about whether the system is alive and requires continuous proof. The reconciliation
loop assumes nothing about whether the configuration is still there and reapplies it forever. In
both cases the improvement came from deleting an assumption, not from adding a feature, and
in both cases the assumption was the same one: <em>that something which was true when I checked it
is still true now.</em></p>
]]></content:encoded>
  </item>
  <item>
    <title>A spirit that gets tired of itself</title>
    <link>https://jan.zorz.si/HA-automations/bored-spirit.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/bored-spirit.html</guid>
    <pubDate>Sun, 06 Sep 2026 12:00:00 +0200</pubDate>
    <description>A spirit that got stuck on two moods for eleven hours and had to be taught fatigue, the concept of night, and how to make a remark about a trip to the toilet.</description>
    <category>house spirit</category>
    <content:encoded><![CDATA[
<p>The house spirit picks its mood from a ladder: the first condition that holds, top to bottom,
wins. That is the right shape for safety (a leak beats everything) and the wrong shape for
personality. On the day a new gadget was bought and somebody used an IQOS indoors, the spirit was
"thrilled" for 376 minutes of a 12-hour window and "annoyed" for 137 more. Together, 71 percent
of the day, two moods, both true, both boring by lunchtime.</p>
<p>A mood engine built on strict priorities has a flaw that only shows up at scale: it is very good at being right and very bad at being interesting. On the day it spent eleven hours alternating between two true moods, the household filed a complaint, and the engine learned to get bored.</p>

<h2>A spirit that gets tired of itself</h2>
<p>The fix is not a smarter ladder. It is fatigue. Every five minutes a script reads the spirit's
own mood history from the recorder, adds up how long each word has been showing in the last two
and a half hours, and publishes the ones that have had twenty minutes or more. The ladder
carries a small guard on each of its "flavour" branches: <code>and 'thrilled' not in
tired</code>.</p>
<p>A tired mood steps aside, the next true branch takes the stage, and when the
sliding window forgets enough of the first mood it can come back. The result is a rotation of
about twenty minutes among whatever is true right now, which is what a person watching the
display experiences as "it changes".</p>
<p>Three details keep it honest. Nothing is stored; the state is derived from history, so it
survives a restart and cannot drift. Hysteresis (tired at twenty minutes, released below
twelve) stops it flapping. And the safety moods (alarm, leak, dangerous air, heat, storm,
illness) never tire; a spirit that got bored of a leak would be a liability. We started with
ten-minute rotation and were told within an hour that it was too fast. Twenty is one number in
one file.</p>

<h2>What time is it, spirit</h2>
<p>The same day's review found the spirit writing "what a lovely day, and I am not alone" at
five in the morning, because "cheerful" sat higher in the ladder than "sleepy" and nothing
below the safety rules knew the hour. Now the ladder knows.</p>
<p>Between eleven at night and five in
the morning a block near the top makes it sleepy if the flat is asleep, tired if somebody is
still up, and bleary if somebody has just walked through the entrance, which is the way to the
toilet. The diary greeting follows the clock: good morning before ten, lovely day, good evening
after six.</p>
<p>The bleary case grew into a proper detector. The way to the toilet is bedroom, living room,
entrance, in that order, and the bedroom has two sensors: a presence radar that holds "on" all
night because somebody is lying there, and a plain motion sensor, the kind that lights the air
monitor's display when you wave at it.</p>
<p>The radar cannot tell "got up" from "asleep". The motion
sensor can, though it also fires twenty times a night on people turning over. As the first link
of a three-room sequence inside ten minutes it is exactly right, and it is useless alone.</p>

<h2>Remarks, on rotation</h2>
<p>A detector that only changes a colour is a sensor. One that comments is a character. So the
toilet trip between three and eight in the morning gets one of four remarks, rotating every ten
minutes so that a second trip does not hear the same joke: "somebody's off to the toilet, and I'm
counting steps instead of sheep"; "through the hall at 4 a.m., bladders do not keep hours".</p>
<p>Somebody awake at four in the living room with an IQOS gets a different set, on the theme of
neither of us sleeping. Whether the remarks are funny is for the household to judge. That they
are <em>situational</em> is the point: they can only appear when the sequence has actually
happened, at an hour when it means what it says.</p>

<p>It now changes its mind every twenty minutes or so, which the household considers lively and the engine, if it could consider anything, would probably consider a loss of principle.</p>
<div class="win"><b>Priority tells you what is true. Fatigue tells you what is interesting.</b>
Keep both, keep safety out of the second, and make the character say things that only its
sensors could know.</div>
]]></content:encoded>
  </item>
  <item>
    <title>Calendars, read-only and distrusted</title>
    <link>https://jan.zorz.si/HA-automations/calendars.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/calendars.html</guid>
    <pubDate>Sun, 06 Sep 2026 12:00:00 +0200</pubDate>
    <description>Getting three calendars into the house without a single OAuth screen, and then distrusting every field of them, one bug at a time.</description>
    <category>calendar</category>
    <content:encoded><![CDATA[
<p>A house spirit that gets up "sleepy" on the morning of a six o'clock flight is not wrong, just
uninformed. The information was in three calendars all along. Getting it into the house turned
out to be a study in small, specific traps.</p>
<p>The house did not know about the six o'clock flight until the morning of the six o'clock flight, at which point it greeted the household as sleepy. It was not wrong. It was uninformed, which is worse, because the information had been sitting in three calendars the whole time.</p>

<h2>Read-only, on purpose</h2>
<p>The calendar provider offers a proper API with OAuth, and a "secret address in iCal format"
for every calendar. We chose the second. The core integration that subscribes to an iCal URL
needs no cloud project, no token refresh and no re-authorisation ritual; it needs a URL kept
in the secrets file. It is also read-only, which we verified in the integration's source rather
than assuming (no create or delete capability declared). If the house ever needs to <em>write</em>
events, the OAuth integration exists and supports both; until then, reading is the whole job.</p>

<h2>Once a day is not a calendar</h2>
<p>The first entry added to a calendar as a test did not appear. The provider's feed had it
within a minute; the integration polls that feed once every twenty-four hours, a constant in its
coordinator, and had last looked at breakfast. An hourly automation now asks each calendar entity
to refresh, which does re-fetch the feed (we tested that with an edited entry rather than
trusting the service description). Three small HTTP requests an hour is a fair price for a
spirit that knows about this afternoon.</p>

<h2>The trip title lies</h2>
<p>The itinerary service publishes a calendar too, and its trip container events carry a title
and a location that name the <em>event</em>: a conference, a city. The actual route is elsewhere,
in the hotel check-in and check-out entries with full addresses, and in the flights.</p>
<p>One upcoming
trip is titled after a conference in one capital and consists entirely of nights in two other
countries. So the route is rebuilt from lodging: pair each check-in with its check-out, take the
city and country from the address, order by date, and only fall back to the title if there is no
lodging at all.</p>
<p>Addresses, naturally, do not agree on where the city goes. Some put it before the postal code,
some after, some at the very front before a comma. The parser looks for known cities first (the
container events helpfully say "is in <em>City</em>, <em>Country</em>", and hotel names usually
contain the city), then for a bare first element without digits, and only then for the token
next to the postal code.</p>
<p>One more trap for the road: the container's end date is exclusive, as
iCal end dates are, so a trip that ends on the second showed as ending on the third until we
subtracted a day. The result is a sensor with days-until-departure and a route string in the
household's language, and the display's morning thoughts say "trip in three days: Zagreb".</p>

<h2>A reminder is not an appointment</h2>
<p>The early-riser rule was simple: any timed event before nine within the next twenty hours
makes the spirit "eager" in the morning instead of sleepy, and exempts a short night from the
sleep score. The first thing it matched was a five-minute reminder to take a vitamin at eight.
Reminders are events too. So an early event must last at least thirty minutes to count; a flight
or an appointment does, a pill does not. The reminder still shows in the day's thoughts, where it
belongs.</p>
<p>And a Jinja footnote that cost twenty minutes: a variable set inside a <code>for</code> loop
does not survive the loop. The thoughts template built its list of calendar lines in a loop over
calendars and emerged with an empty list; the trip line, added outside the loop, appeared alone
and made the bug look like a calendar problem. Unrolling the loop fixed it; a
<code>namespace</code> would have too.</p>

<p>The house now knows where we are going before we have packed. It has not yet offered to help, which we take as a sign of good breeding.</p>
<div class="win"><b>Get the calendar in read-only and make it current; then distrust every
field.</b> Titles name events, not places; end dates are exclusive; reminders are events;
integrations poll slower than you think. Each of those is one line to fix and one afternoon to
find.</div>
]]></content:encoded>
  </item>
  <item>
    <title>A face on the wall</title>
    <link>https://jan.zorz.si/HA-automations/sparky-screen.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/sparky-screen.html</guid>
    <pubDate>Wed, 02 Sep 2026 12:00:00 +0200</pubDate>
    <description>Fifty faces on a display with room for two, a byte order settled by a screenshot, and a lamp that breathes. The spirit gets a body, and immediately gets…</description>
    <category>house spirit</category>
  </item>
  <item>
    <title>Closing the loop on a one-way remote</title>
    <link>https://jan.zorz.si/HA-automations/ac-loop.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/ac-loop.html</guid>
    <pubDate>Wed, 02 Sep 2026 12:00:00 +0200</pubDate>
    <description>An infrared air conditioner cannot answer. A tilt sensor on its louver can. The cheapest possible acknowledgement from a device that has never given one.</description>
    <category>climate</category>
  </item>
  <item>
    <title>Ten readers, one afternoon</title>
    <link>https://jan.zorz.si/HA-automations/ten-readers.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/ten-readers.html</guid>
    <pubDate>Wed, 02 Sep 2026 12:00:00 +0200</pubDate>
    <description>What ten independent readers found in one afternoon, what they got wrong, and why the reader is never the instrument.</description>
    <category>review</category>
  </item>
  <item>
    <title>How the whole system fits together</title>
    <link>https://jan.zorz.si/HA-automations/architecture.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/architecture.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>Layers, protocols, a network sliced into segments, and a second home in another country, all held together by the firm belief that nothing is ever as simple…</description>
    <category>architecture</category>
  </item>
  <item>
    <title>Edge triggers lie to you</title>
    <link>https://jan.zorz.si/HA-automations/level-vs-edge.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/level-vs-edge.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>A pending timer lives in memory, and memory does not survive the restart you perform to fix something else. Edge triggers are promises; level triggers are…</description>
    <category>triggers</category>
  </item>
  <item>
    <title>Sensors that fail healthy</title>
    <link>https://jan.zorz.si/HA-automations/fail-visible.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/fail-visible.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>Sensors here do not fail loudly. They fail 'healthy', holding their last good value with a straight face. How we learned to tell a frozen reading from a calm…</description>
    <category>safety</category>
  </item>
  <item>
    <title>Measure the thing that matters</title>
    <link>https://jan.zorz.si/HA-automations/derived-signals.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/derived-signals.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>Relative humidity, real power and a single voltage reading all mislead, each in its own charming way. The useful numbers are the ones you have to compute…</description>
    <category>sensors</category>
  </item>
  <item>
    <title>Two sources, and what their disagreement means</title>
    <link>https://jan.zorz.si/HA-automations/redundancy.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/redundancy.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>Two sources for everything that matters, and, more importantly, a plan for the day they disagree, because they will, usually at three in the morning.</description>
    <category>data capture</category>
  </item>
  <item>
    <title>Predicting wear instead of reacting to it</title>
    <link>https://jan.zorz.si/HA-automations/lifecycle.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/lifecycle.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>Filters, batteries, motors and scale, predicted instead of discovered, because a household learns about a dead battery at the worst possible moment and about…</description>
    <category>maintenance</category>
  </item>
  <item>
    <title>Alerts you will still trust in a year</title>
    <link>https://jan.zorz.si/HA-automations/alert-hygiene.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/alert-hygiene.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>The rules that keep a notification worth reading a year later: cooldowns, evidence, and the discipline to say nothing when nothing has changed.</description>
    <category>notifications</category>
  </item>
  <item>
    <title>Comfort you did not have to ask for</title>
    <link>https://jan.zorz.si/HA-automations/comfort.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/comfort.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>Comfort you did not ask for: an air conditioner that pre-cools for your return, a fan that knows when nobody is watching, and the etiquette of not being…</description>
    <category>comfort</category>
  </item>
  <item>
    <title>How the house talks to you</title>
    <link>https://jan.zorz.si/HA-automations/interaction.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/interaction.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>How the house talks to you, and how it learned to shut up: phones, a face on the wall, a lamp that breathes, and the rule that every message must be worth…</description>
    <category>interaction</category>
  </item>
  <item>
    <title>Appliances, consumables and upkeep</title>
    <link>https://jan.zorz.si/HA-automations/household.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/household.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>Appliances and upkeep: a dishwasher that reports locally, a washer that only talks to its maker, and the bookkeeping of a flat that would rather not be…</description>
    <category>household</category>
  </item>
  <item>
    <title>Detecting things you never bought a sensor for</title>
    <link>https://jan.zorz.si/HA-automations/detection.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/detection.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>Cooking, exercise, sleep and airflow, all inferred from sensors that were bought for something else entirely and have not been told.</description>
    <category>inference</category>
  </item>
  <item>
    <title>Automations that do not fight each other</title>
    <link>https://jan.zorz.si/HA-automations/arbitration.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/arbitration.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>Twelve automations wanted the same lamp. How they were taught to take turns, and why the loudest one is not always right.</description>
    <category>arbitration</category>
  </item>
  <item>
    <title>Weather and air: nowcasts, not forecasts</title>
    <link>https://jan.zorz.si/HA-automations/weather-air.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/weather-air.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>Nowcasts instead of forecasts: rain in minutes, lightning in kilometres, and a storm outlook that once forgot to expire and kept the house nervous for four…</description>
    <category>environment</category>
  </item>
  <item>
    <title>A system that reports on itself</title>
    <link>https://jan.zorz.si/HA-automations/self-reporting.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/self-reporting.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>Explaining events instead of announcing them, and a nightly review that reads the day back and finds what we would rather not have known.</description>
    <category>reporting</category>
  </item>
  <item>
    <title>A worked example: verifying the wrong thing</title>
    <link>https://jan.zorz.si/HA-automations/verified-wrong.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/verified-wrong.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>How a safety automation, written to stop an air conditioner from running in an empty room, ran the air conditioner in an empty room for four hours, and what…</description>
    <category>case study</category>
  </item>
  <item>
    <title>Do not try this at home</title>
    <link>https://jan.zorz.si/HA-automations/horror-stories.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/horror-stories.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>Ways one installation attacked itself, lied to its own monitoring, and failed in the exact spot it had been built to protect. Told for entertainment; learned…</description>
    <category>field notes</category>
  </item>
  <item>
    <title>Do not try this at home: part two</title>
    <link>https://jan.zorz.si/HA-automations/horror-stories-2.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/horror-stories-2.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>Part two: the tools you fix it with, the dependencies you did not know you had, and the day two independent sources were confidently wrong at the same moment.</description>
    <category>field notes</category>
  </item>
  <item>
    <title>When to open a window</title>
    <link>https://jan.zorz.si/HA-automations/ventilation.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/ventilation.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>Why relative humidity cannot tell you whether to open a window, what to compute instead, and the dew point's quiet superiority over everything else.</description>
    <category>everyday</category>
  </item>
  <item>
    <title>The automations that must never be wrong</title>
    <link>https://jan.zorz.si/HA-automations/safety.html</link>
    <guid isPermaLink="true">https://jan.zorz.si/HA-automations/safety.html</guid>
    <pubDate>Tue, 11 Aug 2026 12:00:00 +0200</pubDate>
    <description>Six rules for the handful of automations that cut power or gate alarms, written by people who have watched each rule get broken at least once.</description>
    <category>everyday</category>
  </item>
</channel>
</rss>
