|
Home IO Control
ESPHome add-on for IO-Homecontrol devices
|
Status: Accepted · Recorded: 2026-09
Amends ADR 0029. Confirmed on a VELUX SSL solar roller shutter: a stop and the status poll after it both land while the motor is travelling, and the motor's own status report is what says it was moving.
ADR 0029 made the start preamble a property of the target's power class: a device declared low_power: true gets LONG_PREAMBLE (1024 bytes, ~213 ms) on every directed start frame, because a duty-cycled receiver needs a long carrier to catch its next wake-up window.
That rule is right for a receiver that is asleep and wrong for one that is awake. A VELUX solar roller shutter that is moving — or has just moved — is not duty-cycling: it is listening continuously, and it ignores the 1024-byte preamble the same way the always-alive VELUX receivers in ADR 0029 do. It answers the short one. Hardware showed both halves on the same motor: with low_power: false a stop mid-travel works, but the motor can then not be started from rest; with low_power: true it starts from rest, but a stop or status poll sent while it moves goes unanswered. Neither fixed setting serves both states, and the state changes within a single command's lifetime.
So the preamble that will be heard depends on the target's state, which the hub can partly infer: it knows when it commanded a move, when a status said "not stopped", and when it last heard the device at all.
D1 — A wake belief per low-power device, derived from evidence the hub already collects. Three levels, ASLEEP, MAYBE_AWAKE, AWAKE (decisions::WakeBelief), computed by one pure function (decisions::wake_belief()) from two stamps and the request:
Moving evidence (IoDevice::last_moving_evidence_ms, written only by note_moving_evidence() and clear_moving_evidence()) is stamped when a movement command is accepted by the device (including one that gets no ack to decode, and on devices with optimistic_state: false), when a decoded status says the device is not stopped, and when a linked remote's movement command is overheard. It is cleared when the device is observed stopped, told to stop, or heard to be stopped by a remote. It is deliberately not stamped by the hub's own optimistic prediction: that is applied before the command is sent, so it would make a resting receiver look awake to the very command that starts it. A command that failed stamps nothing. The other input, last_seen_ms, already existed for link health and is stamped by every frame from a registered device; pairing now stamps it at registration too.
D2 — The belief orders the tries; a multi-try exchange never loses the wake-up preamble. Per try, with short = normal_start_preamble and long = LONG_PREAMBLE:
| Belief | Try 1 | Try 2 | Try 3 |
|---|---|---|---|
| AWAKE | short | long | short |
| MAYBE_AWAKE | short | long | long |
| ASLEEP | long | long | long |
Every plan sends the wake-up preamble at least once. A scheduler-owned status poll is allowed one try at most ladder slots (its backoff ladder is its retry), and that try follows the belief like try 1 of any other exchange. Its likeliest moment is the settle poll seconds after a command or a stop, when the receiver is travelling or has just answered — the state in which it ignores the wake-up preamble — so a fixed wake-up preamble there loses the poll and a backoff slot. A wrong belief on a single try costs that one poll; the ladder's next slot has three tries and includes the wake-up preamble. The settle poll after an accepted stop is allowed all three tries (STOP_SETTLE_POLL_TRIES) whatever the belief — that budget belongs to the poll, not to the belief, which only orders the tries inside it: nothing is moving any more, and the user's reversal waits on its answer. The frame bytes never change between tries; only the transmitter's preamble length does. ExchangeEngine::send_and_receive() resolves the belief once per exchange (plan_request_preamble_()), so it is stable across the tries and the evidence is looked up once.
D3 — The engine decides, the hub supplies evidence. The hub installs a lookup (ExchangeEngine::set_target_evidence_provider()) that returns a device's stamps; the engine derives the belief, checks the tuning switch, and picks the preamble. The whole decision — switch, evidence, STOP shortcut, plan — sits in one place and is testable without a hub.
D4 — Scope. Only a low-power start frame sent by send_and_receive() without an explicit preamble override is affected. Always-alive devices, non-start frames, the 0x3D challenge response, pairing's directed frames (they pass an override), the roll-call and the key-extraction responder are unchanged. request_preamble_for() keeps its single-shot asleep/always-alive rule for callers that send once.
D5 — One diagnostic switch. The low_power_wake_belief tuning parameter, on by default. Off restores the fixed LONG_PREAMBLE on every try to a low-power device, byte- and preamble-identical to the behaviour before this ADR. It is documented as a diagnostic: if a low-power device got worse after updating, set it to false and report.