DEV Community

ilya mozerov
ilya mozerov

Posted on

My pose sensor said someone was home. The phone was just on the charger.

I run a small sensor graph that answers one home-automation question: is someone around? It
reads body motion, room light, and a tamper switch, and emits a single pose label. For weeks the
label RESTING — "parked, but the lights are on, so someone's around" — covered two completely
different situations.

Either the phone was sitting on a charger in a lit room, still because it was docked. Or the
phone was loose in a lit room, still because nobody was touching it. Motion says still. Light
says lit. Neither one tells you which world you are in, and the label had already promised an
answer.

The fix was not a better threshold on motion or light. It was a third fused sense — the charge
state — and a decision I had to make before I deployed it: what is an unreadable charge probe
allowed to mean?

The fusion

The pose pipeline composes three senses into one label. Motion gives a pose tag. Light gives
lit or dark. The new probe reads termux-battery-status over SSH, on a transport the motion
sensor had already proved reachable, and is parsed by a pure function:

classify_power <termux-battery-status json> → plugged | battery | na
Enter fullscreen mode Exit fullscreen mode

PLUGGED_* becomes plugged. UNPLUGGED becomes battery. Anything unparseable, truncated,
or empty becomes na. Then the fusion re-labels only the parked cases:

if [ "$power" = "plugged" ] && { [ "$situ" = "DORMANT" ] || [ "$situ" = "RESTING" ]; }; then
  situ="DOCKED"; gloss="parked on charger (stillness explained by docking, not attendance)"
fi
Enter fullscreen mode Exit fullscreen mode

A carried, handled, or stowed phone on a power bank stays TRAVELLING, HANDLED, or STOWED.
The charger does not override a live interaction, because a phone in your hand on a power bank is
not docked — it is in your hand.

The result is a label no single sensor in the chain produces. Motion says still. Light says lit.
Charge says tethered. Together they say docked, not attended.

The rule I almost skipped

Here is the part worth writing down. The probe is over a network, to a phone, over SSH. It will
be unreachable. So before deploying I had to decide what power=n/a was allowed to mean, and
there were two defensible answers:

  • n/a means on battery. Convenient: the label degrades to the old RESTING answer and nothing downstream changes.
  • n/a means I do not know, and the label does not move.

I picked the second, and it is the rule the whole fusion rests on:

An unreachable charge probe renders power=n/a, the pose label stays unchanged, and earns
no docked marker.

na is visibly distinct from battery. An unreadable charge read is not "on battery". A
failed probe cannot manufacture the docked claim.

The sibling case in the same code made me pick this twice. The tool already had a tamper axis
with the same shape: tamper=na must not carry "tamper-clear", and must render differently from
tamper=quiet. Once you have one axis where an unreachable input is forbidden from producing a
stronger claim than a read one, the second axis is not a judgement call — it is the same rule.

The falsifiable surface

Every behaviour above is pinned to an assertion in the tool's test suite, each a one-line check
that goes red if the fusion silently degrades:

# the fusion
derive_situ '[body-still]' 0 "" "plugged"   → DOCKED
derive_situ '[body-still]' 1 "" "plugged"   → DOCKED
derive_situ '[body-still]' 0 "" "battery"   → RESTING   (unchanged)
derive_situ '[body-still]' 0 "" "na"        → RESTING   (unchanged — unreachable earns nothing)
derive_situ '[body-still]' 0 "" ""          → RESTING   (backward compatible; default unchanged)

# the charger never overrides a live interaction
derive_situ '[body-carried]'  0 "" "plugged"          → TRAVELLING
derive_situ '[body-handled]'  0 "moved" "plugged"     → INTERACTED

# the probe parser
classify_power '{"plugged":"PLUGGED_AC"}'             → plugged
classify_power '{"plugged":"UNPLUGGED"}'              → battery
classify_power 'garbage'                              → na
classify_power ''                                     → na
Enter fullscreen mode Exit fullscreen mode

The na assertion is the load-bearing one. Drop the na clause from the fusion and exactly that
leg goes red while every green-verdict case stays green — which is the whole point. A test suite
that only asserts the happy path would pass on code that silently manufactures docked claims
whenever the probe fails.

The na path has its own test that drove me to fix the tool itself. An unreachable body organ
used to exit 2 in total silence: no verdict on stdout, so a downstream $(...) capture read an
empty string — indistinguishable from a crashed or killed process. Now the unreachable path
speaks:

[body-context] n/a — phone unreachable, so body context is UNREAD (NOT 'calm', NOT 'still')
Enter fullscreen mode Exit fullscreen mode

It exits 2 still, so a caller can gate on it. But it names what is unreachable and says what it
is not, on the channel a consumer actually reads. The test asserts a verdict lands on stdout,
asserts it names the unreachable thing, asserts it uses NOT-vocabulary rather than all-clear
vocabulary — and asserts it a second consecutive time, because the old suppression was
once-only. A test that ran the path once would have passed on the broken code.

What I verified, and when

The live measurement was taken on 2026-09-21, at commit a0763444, when the phone was docked:

scripts/mesh-body-context --power   rc=0
  DOCKED — parked on charger (stillness explained by docking, not attendance)
  | lux=10 (lit) | power=plugged
Enter fullscreen mode Exit fullscreen mode

Motion still, room lit, probe plugged — the emitted label was DOCKED, which no single sensor
in that chain gives.

While drafting, the phone went unreachable. That is the power=n/a regime, so it got verified
live too:

scripts/mesh-body-context --power   rc=0
  [body-context] n/a — phone unreachable, so body context is UNREAD
  (NOT 'calm', NOT 'still')

scripts/mesh-body-context --test     derive-logic + n/a-path-speaks ok
  (offline pose fixtures + n/a-path-speaks assertions, phone-independent)
Enter fullscreen mode Exit fullscreen mode

Both arms of the honesty rule — the docked claim when the probe reads, and the refusal to
manufacture one when it does not — were exercised in the same session.

What I did not verify

  • This is one axis on one node. I proved the fusion's decision logic and its live emission. I did not measure how DOCKED behaves across a long dock→undock transition, and I make no claim about how downstream consumers react to the new label. No consumer contract changed.
  • The endpoint flaps. The phone's SSH endpoint went OK → TIMEOUT → REFUSED inside about 25 minutes on 2026-09-20. I did not reproduce that flap, and power=n/a is exactly what renders during it. The claim here is about how the label behaves when the probe is down, not a claim that the transport is reliable.
  • The probe reads plugged alone. A battery at 100% and a battery at 3% are both battery to this fusion. Charge level is not an input, and no low-battery claim is made.

The shape of it

The generalisable failure is a pose label silently disambiguated by an input it did not have.

Two different situations collapsed onto one label because the senses available could not
separate them. The corrective was to name the input that would disambiguate, read it, and — the
part that transfers hardest — decide before deployment what an unreadable version of it was
allowed to mean.

If your label is "someone is around" and your inputs are motion and light, you have not measured
attendance. You have measured stillness-in-light. And when you add the missing sense, the
tempting failure is to let its absence degrade to whichever answer is convenient, because that
keeps every downstream consumer quiet.

Here an unreadable charge state is allowed to mean the label you already had, and is forbidden
from meaning docked. It is a weaker answer than a read one, and that asymmetry — never let an
unreachable input produce a stronger claim than a read one — is the whole rule.

Top comments (0)