|
Home IO Control
ESPHome add-on for IO-Homecontrol devices
|
Classes | |
| struct | OneWayDedupState |
| Key fields of the last processed 1W frame, used to collapse a remote's repeat burst. More... | |
| struct | TargetEvidence |
| What the hub knows about the device an exchange is addressed to, as the exchange engine sees it. More... | |
Enumerations | |
| enum class | ExchangeFirstResponseDisposition : uint8_t { IGNORE_UNRELATED , COMPLETE_DIRECT , REQUIRE_AUTH } |
| Disposition for the first response in an authenticated exchange. More... | |
| enum class | ExchangeFinalResponseDisposition : uint8_t { IGNORE_UNRELATED , ACCEPT } |
| Disposition for the final response after authentication. More... | |
| enum class | PairingDiscoveryDisposition : uint8_t { NO_RESPONSE , INVALID , ACCEPT } |
| Disposition during pairing discovery phase. More... | |
| enum class | PairingKeyChallengeDisposition : uint8_t { IGNORE , ACCEPT } |
| Disposition during pairing key-challenge phase. More... | |
| enum class | PairingDiscoverConfirmDisposition : uint8_t { IGNORE , ACK , ERROR } |
| Disposition for a candidate reply to a discovery-confirm request (0x2C). More... | |
| enum class | PairingKeyConfirmDisposition : uint8_t { IGNORE , CONFIRM , CHALLENGE , REFUSE } |
| Disposition for a candidate reply to the key-transfer (0x32) confirm wait — the slow-turnaround path's expanded classification of PairingKeyChallengeDisposition's device challenge to also cover a direct confirm or an explicit refusal. More... | |
| enum class | WakeBelief : uint8_t { ASLEEP , MAYBE_AWAKE , AWAKE } |
| How likely a low-power receiver is to be awake right now, judged from what this hub has seen of it. More... | |
Functions | |
| bool | is_exchange_internal_command (uint8_t cmd) |
| Returns true for commands that are internal to an exchange handshake and carry no useful information for a passive observer (challenge request/response). | |
| bool | frame_matches_nodes (const IoFrame &frame, const uint8_t expected_src[NODE_ID_SIZE], const uint8_t expected_dst[NODE_ID_SIZE]) |
| Check if two frames have identical src/dst node IDs. | |
| bool | frame_matches_exchange_endpoints (const IoFrame &request, const IoFrame &candidate) |
| Check if candidate frame endpoints are the reverse of the request (dst==request.src, src==request.dst). | |
| ExchangeFirstResponseDisposition | classify_exchange_first_response (const IoFrame &request, const IoFrame &candidate) |
| Decide how to handle the first response packet in an authenticated exchange. | |
| ExchangeFinalResponseDisposition | classify_exchange_final_response (const IoFrame &request, const IoFrame &candidate) |
| Decide if a candidate frame is an acceptable final response after authentication. | |
| bool | is_repeatable_execute (const IoFrame &request) |
| True for a CMD_EXECUTE that has the same effect sent twice as sent once. | |
| bool | retry_after_unconfirmed_accept_is_safe (const IoFrame &request, bool target_confirms_execute, uint8_t unconfirmed_tries) |
| Whether an authenticated-but-unanswered request may be sent again. | |
| PairingDiscoveryDisposition | classify_pairing_discovery_response (const IoFrame &candidate, const uint8_t controller_id[NODE_ID_SIZE]) |
| Decide if a frame is a valid discovery response (0x29) during pairing. | |
| PairingKeyChallengeDisposition | classify_pairing_key_challenge (const IoFrame &candidate, const uint8_t device_id[NODE_ID_SIZE], const uint8_t controller_id[NODE_ID_SIZE]) |
| Decide if a frame is a valid key-challenge (0x3C) during pairing key exchange. | |
| PairingDiscoverConfirmDisposition | classify_pairing_discover_confirm_reply (const IoFrame &candidate, const uint8_t device_id[NODE_ID_SIZE], const uint8_t controller_id[NODE_ID_SIZE]) |
| Decide how to handle a candidate reply to a discovery-confirm request (0x2C) during pairing. | |
| PairingKeyConfirmDisposition | classify_pairing_key_confirm_reply (const IoFrame &request, const IoFrame &candidate) |
| Decide how to handle a candidate reply to the key-transfer (0x32) confirm wait (wait_for_key_confirm_(), slow-turnaround chips only). | |
| bool | discover_confirm_try_rotates (uint8_t try_index) |
| Whether discover-confirm try try_index (1-based) should rotate channels rather than hold the request channel. | |
| bool | is_duplicate_1w_frame (const OneWayDedupState &last, const OneWayDedupState &incoming, uint32_t window_ms) |
| Decide whether an incoming 1W frame repeats the previous one inside the burst window. | |
| bool | defer_background_poll_for_1w_activity (bool next_op_is_background, uint32_t first_1w_activity_ms, uint32_t last_1w_activity_ms, uint32_t now, uint32_t quiet_ms, uint32_t max_defer_ms) |
| Decide whether to hold back a queued background poll because a 1W remote is still transmitting. | |
| uint8_t | scheduled_poll_max_tries (uint8_t status_poll_failures, uint8_t auth_poll_failures, bool settles_a_stop=false) |
| Transmit-attempt budget for a scheduler-owned status poll, by backoff-ladder position. | |
| TargetEvidence | target_evidence (const IoDevice &dev) |
| Build the exchange engine's view of a device record. | |
| bool | is_stop_request (const IoFrame &request) |
| True for a CMD_EXECUTE whose main byte is POS_STOP. | |
| WakeBelief | wake_belief (const TargetEvidence &evidence, uint32_t now, bool is_stop) |
| Judge how awake a low-power receiver is. | |
| uint16_t | low_power_try_preamble (WakeBelief belief, uint8_t try_index, uint16_t short_preamble) |
| Start-preamble length for one try of a directed exchange to a low-power receiver. | |
| const char * | wake_belief_name (WakeBelief belief) |
| Lowercase name of a belief for log lines. | |
| bool | is_one_way_pairing_gesture (bool oneway, const uint8_t dst[NODE_ID_SIZE], uint8_t cmd) |
| True if a frame's shape matches a 1W remote's pairing gesture (issue #27/#65): CTRL0 1W bit set, addressed to the 1W broadcast address (0x00003F), with one of the three command bytes observed in the field capture — 0x20 (WRITE_PRIVATE), 0x39 (1W remove), or 0x2E (alternate discovery, 1W-flagged). | |
| bool | oneway_burst_started_fresh (uint32_t last_1w_activity_ms, uint32_t now, uint32_t quiet_ms) |
| Whether a 1W frame arriving at now starts a new burst rather than extending the current one — true if no frame has been seen yet, or the gap since the last one reached quiet_ms (the previous burst already released any deferred poll). | |
| uint32_t | remote_poll_timer_id (const uint8_t node_id[NODE_ID_SIZE]) |
| Numeric set_timeout() id for a device's remote-activity poll timer (hub_status.cpp's schedule_status_poll_()). | |
Variables | |
| constexpr uint32_t | REMOTE_POLL_TIMER_ID_TAG = 0x01000000 |
| Namespace tag for remote_poll_timer_id() below: the node address occupies the low 24 bits, and this is OR-ed in above them. | |
|
strong |
Disposition for the final response after authentication.
| Enumerator | |
|---|---|
| IGNORE_UNRELATED | Frame doesn't match endpoints — ignore. |
| ACCEPT | Frame matches expected response — exchange succeeds. |
Definition at line 33 of file hub_decisions.h.
|
strong |
Disposition for the first response in an authenticated exchange.
Definition at line 26 of file hub_decisions.h.
|
strong |
Disposition for a candidate reply to a discovery-confirm request (0x2C).
Definition at line 52 of file hub_decisions.h.
|
strong |
Disposition during pairing discovery phase.
| Enumerator | |
|---|---|
| NO_RESPONSE | No packets received on the channel within timeout. |
| INVALID | Packets seen but none were valid discovery (0x29) frames. |
| ACCEPT | Valid discovery response received. |
Definition at line 39 of file hub_decisions.h.
|
strong |
Disposition during pairing key-challenge phase.
| Enumerator | |
|---|---|
| IGNORE | Not a valid challenge (wrong cmd, length, or sender). |
| ACCEPT | Valid 0x3C challenge from target device. |
Definition at line 46 of file hub_decisions.h.
|
strong |
Disposition for a candidate reply to the key-transfer (0x32) confirm wait — the slow-turnaround path's expanded classification of PairingKeyChallengeDisposition's device challenge to also cover a direct confirm or an explicit refusal.
| Enumerator | |
|---|---|
| IGNORE | Not from/to the expected endpoints — keep waiting. |
| CONFIRM | CMD_KEY_CONFIRM (0x33) — the device accepted the key. |
| CHALLENGE | A fresh CMD_CHALLENGE_REQ (0x3C) — the device may challenge the key transfer before confirming it; answered via ExchangeEngine::answer_challenge(). |
| REFUSE | Anything else (including CMD_ERROR_RESP) — an explicit or implicit refusal; ends the whole wait, not just the current try. |
Definition at line 62 of file hub_decisions.h.
|
strong |
How likely a low-power receiver is to be awake right now, judged from what this hub has seen of it.
Orders the tries of a directed exchange: an awake receiver hears the short start preamble and ignores the long wake-up one, a sleeping one needs the long one.
Definition at line 386 of file hub_decisions.h.
|
inline |
Decide if a candidate frame is an acceptable final response after authentication.
Only endpoint matching is checked here; command validity is encoded in the disposition mapping by the caller.
| request | Original outbound request frame. |
| candidate | Parsed IoFrame from the device. |
Definition at line 124 of file hub_decisions.h.
|
inline |
Decide how to handle the first response packet in an authenticated exchange.
Used by wait_for_first_response_() to determine whether the exchange:
| request | Original outbound request frame. |
| candidate | Parsed IoFrame from the device. |
Definition at line 105 of file hub_decisions.h.
|
inline |
Decide how to handle a candidate reply to a discovery-confirm request (0x2C) during pairing.
A non-matching or unrecognised frame is IGNORE, keeping the listen open rather than ending it: the discover-confirm step never fails a pairing attempt (see run_discover_confirm_step_()'s doc) — this classification only decides whether to keep listening or stop early.
| candidate | Parsed IoFrame. |
| device_id | Node ID of the device being paired (expected sender). |
| controller_id | Node ID of this controller (expected destination). |
Definition at line 223 of file hub_decisions.h.
|
inline |
Decide if a frame is a valid discovery response (0x29) during pairing.
Only the destination is checked, not the source: the discovery request goes out to a shared broadcast address, so a response arriving during the same window may be a device answering a different controller's concurrent discovery rather than ours — real hardware addresses its response back to the requesting controller's own node ID, so checking that is both possible and sufficient to reject it. The source can't be checked here — the device's node ID is exactly what discovery exists to learn, so there is nothing yet to compare it to.
| candidate | Parsed IoFrame. |
| controller_id | Node ID of this controller (expected destination). |
Definition at line 182 of file hub_decisions.h.
|
inline |
Decide if a frame is a valid key-challenge (0x3C) during pairing key exchange.
The challenge must:
| candidate | Parsed IoFrame. |
| device_id | Node ID of the device being paired (expected sender). |
| controller_id | Node ID of this controller (expected destination). |
Definition at line 201 of file hub_decisions.h.
|
inline |
Decide how to handle a candidate reply to the key-transfer (0x32) confirm wait (wait_for_key_confirm_(), slow-turnaround chips only).
| request | The outbound CMD_KEY_TRANSFER (0x32) this reply answers. |
| candidate | Parsed IoFrame from the device. |
Definition at line 243 of file hub_decisions.h.
|
inline |
Decide whether to hold back a queued background poll because a 1W remote is still transmitting.
The radio is half-duplex and an authenticated exchange blocks for 1–3 s, during which no frame can be received at all. A press on a linked remote schedules a status poll, so without this gate the hub's own poll can start on top of the burst that triggered it and go deaf to the rest of it.
Only background polls are deferred. A user command must never wait on a remote the user may not even own — 1W broadcasts carry no ownership marker, so the activity could be a neighbour's.
The hold re-arms on every 1W frame received while it is already active, so a real burst from one remote (~160 ms, well under quiet_ms) never gets cut short mid-transmission. Left unchecked that re-arming has no cap: sustained sub-quiet_ms 1W traffic from any source — including a neighbour's, since these broadcasts carry no ownership marker — would hold background polls back indefinitely. max_defer_ms bounds that: once that much time has passed since the burst started (not the most recent frame), the poll is let through regardless of ongoing traffic. The gate only ever delays a poll, never drops one — it stays queued and fires as soon as it is no longer deferred.
| next_op_is_background | True if the queue front is a REQUEST_STATUS / REQUEST_NAME. |
| first_1w_activity_ms | millis() of the first frame in the current 1W burst; 0 if none seen since boot. |
| last_1w_activity_ms | millis() of the most recent 1W frame; 0 if none seen since boot. |
| now | Current millis(). |
| quiet_ms | How long after 1W activity to hold background polls back. |
| max_defer_ms | Hard cap on total defer time, measured from first_1w_activity_ms. |
Definition at line 341 of file hub_decisions.h.
|
inlinenodiscard |
Whether discover-confirm try try_index (1-based) should rotate channels rather than hold the request channel.
Tries 1 and 3 hold: a 0x2D is a unicast reply to a unicast 0x2C, and every other unicast pairing wait in this project holds the request channel on that same expectation (see listen_for_key_confirm_()'s own reasoning), and every 0x2D a Somfy Izymo dimmer sent came back on the request channel. But the one real VELUX-system sample on record (from a hopping monitor, a weak source) logged a 0x2D on a different channel than its own 0x2C, so the middle try hedges by rotating instead, until a VELUX device's reply channel is measured. See ADR 0039 for why this is not simply one fixed policy for every try, unlike every other listen in this project (ADR 0028).
| try_index | 1-based try number (1..PAIRING_DISCOVER_CONFIRM_TRIES). |
Definition at line 268 of file hub_decisions.h.
|
inline |
Check if candidate frame endpoints are the reverse of the request (dst==request.src, src==request.dst).
Definition at line 89 of file hub_decisions.h.
|
inline |
Check if two frames have identical src/dst node IDs.
Definition at line 82 of file hub_decisions.h.
|
inline |
Decide whether an incoming 1W frame repeats the previous one inside the burst window.
| last | State recorded for the previously processed 1W frame. |
| incoming | Candidate frame's key fields, with timestamp set to now. |
| window_ms | Burst-suppression window. |
Definition at line 303 of file hub_decisions.h.
|
inline |
Returns true for commands that are internal to an exchange handshake and carry no useful information for a passive observer (challenge request/response).
These frames appear in every authenticated exchange between other controllers and devices on the network, but contain only ephemeral cryptographic data.
Definition at line 77 of file hub_decisions.h.
|
inline |
True if a frame's shape matches a 1W remote's pairing gesture (issue #27/#65): CTRL0 1W bit set, addressed to the 1W broadcast address (0x00003F), with one of the three command bytes observed in the field capture — 0x20 (WRITE_PRIVATE), 0x39 (1W remove), or 0x2E (alternate discovery, 1W-flagged).
Shared between PairingAdvisor (classifying recorded telemetry events, pairing_advisor.cpp) and the hub's normal passive RX path (hub_status.cpp), which remembers a recent sighting so a PROG press completed just before "Discover & Pair" is pressed isn't invisible to the advisor purely because of when the discovery telemetry window happened to open — see PairingTelemetry::record_recent_one_way_sighting().
| oneway | CTRL0 1W-protocol bit. |
| dst | Frame destination node ID. |
| cmd | Frame command byte. |
Definition at line 486 of file hub_decisions.h.
|
inlinenodiscard |
True for a CMD_EXECUTE that has the same effect sent twice as sent once.
Every EXECUTE this hub sends names an absolute target (a position, open/close, STOP, a tilt angle, a light level) except the stored-position selector POS_FAVORITE, used by favourite and vent: on a Somfy motor "My" while moving means stop, so a second copy can undo what the first one started.
| request | Outbound request frame. |
Definition at line 135 of file hub_decisions.h.
|
inlinenodiscard |
True for a CMD_EXECUTE whose main byte is POS_STOP.
A STOP is only ever sent to a receiver that is (believed) moving, so it is treated as awake whatever the stamps say.
| request | Outbound request frame. |
Definition at line 416 of file hub_decisions.h.
|
inlinenodiscard |
Start-preamble length for one try of a directed exchange to a low-power receiver.
The plans (short = short_preamble, LONG = LONG_PREAMBLE): AWAKE short/LONG/short, MAYBE_AWAKE short/LONG/LONG, ASLEEP LONG on every try — so an exchange allowed more than one try still tries the wake-up preamble at least once, and a wrong belief costs one try, not the exchange. A single-try exchange (most scheduler-owned status polls) sends only try 1's preamble; its backoff ladder, whose three-try slots include the wake-up preamble, covers a wrong belief there.
| belief | See wake_belief(). |
| try_index | 1-based try number, clamped to [1, EXCHANGE_RETRY_COUNT]. |
| short_preamble | Preamble for a receiver known to be awake (normal_start_preamble). |
Definition at line 448 of file hub_decisions.h.
|
inlinenodiscard |
Whether a 1W frame arriving at now starts a new burst rather than extending the current one — true if no frame has been seen yet, or the gap since the last one reached quiet_ms (the previous burst already released any deferred poll).
Callers use this to decide whether to reset a burst's start-time tracking; see defer_background_poll_for_1w_activity() for why the burst start (not just the latest frame) needs its own timestamp.
| last_1w_activity_ms | millis() of the most recent 1W frame before this one; 0 if none seen since boot. |
| now | Current millis() (this frame's arrival time). |
| quiet_ms | Gap after which a previous burst is considered over. |
Definition at line 504 of file hub_decisions.h.
|
inline |
Numeric set_timeout() id for a device's remote-activity poll timer (hub_status.cpp's schedule_status_poll_()).
Keyed by the device's node address rather than a name, because Component::set_timeout(const char *name, ...) stores the caller's pointer, not a copy of the string (its header documents this: static lifetime required, use the numeric-id overload for a dynamically-built name) — see schedule_status_poll_()'s own comment for why a per-device name can't satisfy that here. A node address is unique per device, so this id is collision-free by construction.
| node_id | 3-byte device node address. |
Definition at line 525 of file hub_decisions.h.
|
inlinenodiscard |
Whether an authenticated-but-unanswered request may be sent again.
Every request except CMD_EXECUTE is idempotent (status polls, name reads, management actions, config writes) and keeps its full retry budget when the device authenticates but never closes the exchange.
CMD_EXECUTE moves something, and a missing closing reply means two different things depending on the device. Some devices never close an EXECUTE exchange within the response window and report through a later status update instead; for them silence is normal and a re-send only repeats a command already being carried out. For a device that normally does close it, silence can mean our challenge answer never arrived and the command was not carried out at all (a STOP that left an awning moving). So an EXECUTE is sent again only to a device known to confirm, only when repeating it is harmless (is_repeatable_execute()), and at most UNCONFIRMED_EXECUTE_MAX_RESENDS times per exchange.
| request | Outbound request frame. |
| target_confirms_execute | The target has closed an EXECUTE exchange with a reply before (TargetEvidence::confirms_execute). |
| unconfirmed_tries | Tries of this exchange that ended accepted without a closing reply, including the one just ended (1-based). |
Definition at line 160 of file hub_decisions.h.
|
inline |
Transmit-attempt budget for a scheduler-owned status poll, by backoff-ladder position.
See SCHEDULED_POLL_MAX_TRIES and SCHEDULED_POLL_RETRY_GRACE_FIRST_FAILURE (proto_timing.h) for why the full budget belongs to a middle band of the ladder rather than to its start or its tail.
The two counters are mutually exclusive by construction — StatusPollPolicy::on_exchange_failed() zeroes one while incrementing the other — so an auth-shaped streak reads status_poll_failures as 0 and would otherwise fall into the band's own "fresh window" case. It is rejected first, deliberately, so the predicate stays correct even if that exclusivity is ever relaxed.
The settle poll after an accepted STOP (settles_a_stop) gets the full budget whatever the counters say: see STOP_SETTLE_POLL_TRIES (proto_timing.h).
| status_poll_failures | Consecutive silent failures already recorded for this device. |
| auth_poll_failures | Consecutive challenge-seen failures already recorded. |
| settles_a_stop | True for the first poll after an accepted STOP (StatusPollPolicy::take_stop_settle()). |
Definition at line 369 of file hub_decisions.h.
|
inlinenodiscard |
Build the exchange engine's view of a device record.
| dev | Device record to read. |
Definition at line 409 of file hub_decisions.h.
|
inlinenodiscard |
Judge how awake a low-power receiver is.
AWAKE for a STOP request or while moving evidence is younger than LOW_POWER_MAX_TRAVEL_MS; otherwise MAYBE_AWAKE while the newest of moving evidence and last frame heard is younger than LOW_POWER_AWAKE_HOLD_MS; otherwise ASLEEP. A zero stamp means never and is never recent. Ages use unsigned subtraction, so millis() wrap-around is safe.
| evidence | Stamps for the target device. |
| now | Current millis(). |
| is_stop | True when the request being sent is a STOP (see is_stop_request()). |
Definition at line 428 of file hub_decisions.h.
|
inlinenodiscard |
Lowercase name of a belief for log lines.
Definition at line 462 of file hub_decisions.h.
|
constexpr |
Namespace tag for remote_poll_timer_id() below: the node address occupies the low 24 bits, and this is OR-ed in above them.
Nothing else uses Component::set_timeout's numeric-id overload today, so the tag has no collision to avoid yet — it exists so the id space stays self-describing (which caller a given id belongs to) the day a second numeric-id timer is added.
Definition at line 512 of file hub_decisions.h.