|
Home IO Control
ESPHome add-on for IO-Homecontrol devices
|
Status: Accepted · Recorded: 2026-08
The hub accumulated several largely-independent responsibilities: authenticated exchanges, discovery and pairing, administrative actions, the device table, operation coalescing, poll scheduling, and pairing telemetry.
Holding all of that in one class risks unbounded growth and — worse — entangles unrelated state machines through shared internal state, so a change to poll scheduling can affect pairing for reasons no one intended.
Each concern is owned by its own collaborator object, held by value inside the hub component. The hub keeps only lifecycle, wiring, and dispatch.
flowchart TB
HA["Home Assistant · ESPHome entities"]
OQ["OperationQueue<br/><i>queue + coalescing</i>"]
MA["ManagementActions<br/><i>rename · identify · force-open</i>"]
EE["ExchangeEngine<br/><i>authenticated exchange, inbound auth</i>"]
PE["PairingEngine<br/><i>discovery + key exchange</i>"]
OT["OneWayTransmitter<br/><i>1W bursts, awaits nothing</i>"]
KX["KeyExtractionResponder<br/><i>device-role responder</i>"]
R(["RadioDriver"])
DR["DeviceRegistry<br/><i>device table + callback fan-out</i>"]
SP["StatusPollPolicy<br/><i>poll scheduling + backoff</i>"]
PT["PairingTelemetry<br/><i>per-attempt event record</i>"]
KA["OnewayKeyAdoption<br/><i>receive-only key adoption</i>"]
FU["Lr1121FirmwareUpdateController<br/><i>compile-gated flash orchestration</i>"]
HA -->|commands| OQ
OQ -->|"loop() drains, one at a time"| MA
OQ --> EE
OQ --> PE
OQ --> OT
MA --> EE
EE --> R
PE --> R
OT --> R
KX --> R
FU -.->|SpiAccess, boot + button| R
EE --> DR
EE --> PT
PE --> PT
DR -->|state callbacks| HA
SP -->|"due devices re-enqueued"| OQ
classDef radio stroke:#2d6a4f,stroke-width:3px
classDef state stroke:#5c6785,stroke-width:3px
classDef gated stroke:#2d6a4f,stroke-width:3px,stroke-dasharray:5 4
class MA,EE,PE,OT,KX radio
class DR,SP,PT,KA state
class FU gated
Green marks the collaborators that reach the radio; the queue above the outbound ones guarantees only one transmits at a time (KeyExtractionResponder replies from the receive path, not the queue, but is self-gated so it only transmits while explicitly armed). Grey marks the ones that only hold state and are driven by the hub — receive-side handling updates the registry and the poll policy from the same frame, but neither calls the other. Lr1121FirmwareUpdateController is dashed because it exists only when lr1121_firmware_update: is configured, and reaches the radio through SpiAccess in bootloader mode rather than the driver.
The hub file itself splits the same way: lifecycle and loop, queued dispatch, and passive receive-side handling, plus thin wrappers over the collaborators. Where a collaborator needs a protected hub capability (set_timeout, transmit_frame_, warn_if_blocking_over_, busy_), the hub injects a std::function built in its own member-initializer list rather than granting friend — the same pattern OneWayTransmitter established for transmit_frame_. The shared aliases for those callbacks live in hub_hooks.h, a dependency-light header that never includes hub_core.h.