|
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 | AttemptPlan |
| How one exchange spends its attempts. Handed to ExchangeEngine::send_and_receive(). More... | |
| struct | AttemptRequest |
| What a caller asks of one exchange's attempts: a plain cap on tries (today's shape), or a read kind whose plan the engine builds for the request (ExchangeEngine::plan_read()). More... | |
| struct | TargetEvidence |
| What the hub knows about the device an exchange is addressed to, as the exchange engine sees it. More... | |
| struct | RainLimitationInput |
| What one decoded status reply said, plus what the hub had predicted. More... | |
| struct | StoppedReportInput |
| One decoded status report, plus what the hub remembers and whether it will poll again. 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 | ReadKind : uint8_t { PROGRESS , SETTLING , COMMAND } |
| What a request is for, which decides how its attempts are spent. More... | |
| enum class | WakeBelief : uint8_t { ASLEEP , AWAKE , UNKNOWN } |
| Whether a low-power receiver is believed to be travelling right now, judged from what this hub has seen of it. More... | |
| enum class | HeaterModelHint : uint8_t { NONE , LOOKS_LIKE_RADIATOR , LOOKS_LIKE_TOWEL_DRYER } |
| What the model check suggests about a heater's declared io_model:. More... | |
| enum class | RainLimitationRule : uint8_t { NONE = 0 , RAIN_ORIGINATOR , CLAMPED_COMMAND } |
| Which evidence path labelled a device as limited by rain. More... | |
| enum class | StoppedReport : uint8_t { MOVING , STOPPED , STOPPED_CONFIRMED , STOPPED_UNCONFIRMABLE , SETTLING } |
| What the hub makes of one status report's movement state. 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 | is_device_event_report (const IoFrame &frame) |
| Returns true for a frame that reports a device event rather than a status: a CMD_SET_CONFIG1_RESP, or a well-formed CMD_STATUS_UPDATE whose event is not EVENT_STATUS_CHANGE (a subscribed sensor report, see decode_status_update_event()). | |
| bool | is_subscription_answer (const IoFrame &request, const IoFrame &response) |
Returns true when response is the answer to the subscription request: the request is a CMD_SUBSCRIBE_EVENT with the subscribe action, and the response a well-formed CMD_STATUS_UPDATE carrying the same event, from the device the request went to. | |
| bool | overheard_command_writes (uint8_t cmd) |
| Returns true for the commands that write to a device: an execute, a mode activation or a private register write. | |
| 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. | |
| bool | scheduled_poll_in_retry_band (uint8_t status_poll_failures, uint8_t auth_poll_failures) |
| True when a scheduler-owned status poll sits in the middle band of the backoff ladder. | |
| uint8_t | scheduled_poll_max_tries (uint8_t status_poll_failures, uint8_t auth_poll_failures) |
| Transmit-attempt budget for a scheduler-owned status poll of a heater, by ladder position. | |
| uint32_t | short_lead_cost_ms (uint32_t short_tx_ms) |
| What the short lead attempt costs the exchange: its transmission, its brief listen and the attempt overhead (the same terms attempt_fits() counts for any attempt). | |
| bool | attempt_fits (uint32_t elapsed_ms, uint32_t gap_ms, uint32_t tx_ms, uint32_t window_ms, uint32_t auth_tail_ms, uint32_t budget_ms) |
The attempt budget rule: an attempt starts only if the whole attempt, up to the end of the last wait it can make, ends inside budget_ms. | |
| uint8_t | attempts_that_fit (uint32_t budget_ms, uint32_t attempt_tx_ms, uint32_t early_window_ms, uint32_t last_window_ms, uint8_t cap) |
How many attempts to a silent target fit the budget when none waits a gap, capped at cap. | |
| AttemptPlan | attempt_plan_for_tries (uint8_t max_tries, uint16_t window_ms) |
The plan for a caller that names only a number of attempts: that many (at least one, at most EXCHANGE_RETRY_COUNT), the default gap, and window_ms. | |
| AttemptPlan | attempt_plan (ReadKind kind, bool low_power, const TuningConfig &tuning, uint32_t attempt_tx_ms, uint16_t last_window_ms=0, std::optional< uint32_t > short_lead_tx_ms=std::nullopt) |
| The attempt plan for a request, from what it is for and who it asks. | |
| bool | is_repeat_safe_command (const IoFrame &request) |
| Whether a request may be sent again to a device that said nothing, without a second action. | |
| ReadKind | scheduled_poll_read_kind (uint8_t status_poll_failures, uint8_t auth_poll_failures, bool settles_a_position) |
| What a cover's status poll is for, from where it sits and what it settles. | |
| TargetEvidence | target_evidence (const IoDevice &dev) |
| Build the exchange engine's view of a device record. | |
| bool | moving_until_holds (const TargetEvidence &evidence, uint32_t now) |
| True while the remaining time the receiver last reported has not run out: by its own account it is still travelling, so a silent exchange is not evidence that it has come to rest. | |
| 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, bool is_status_read=false, bool state_unknown=false) |
| Judge whether a low-power receiver is travelling. | |
| uint16_t | low_power_try_preamble (WakeBelief belief, uint8_t try_index, uint16_t short_preamble, uint16_t wake_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). | |
| std::array< uint32_t, 3 > | status_update_ack_channel_order (uint32_t receive_freq_hz) |
| The order in which a 0x72 status-update acknowledgement is sent on the three channels: the channel the 0x71 exchange ran on first, then the other two in their usual CH1, CH2, CH3 order. | |
| bool | frame_issue_is_warning (bool dst_registered, bool dst_is_us) |
| Whether a frame issue (a frame the hub did not act on) is logged at WARN rather than DEBUG. | |
| 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_()). | |
| uint32_t | heating_poll_timer_id (const uint8_t node_id[NODE_ID_SIZE]) |
| Numeric set_timeout() id for a heater's regular-cadence poll timer (IOHomeControlComponent::schedule_heating_poll_()). | |
| HeaterModelHint | classify_heater_model_hint (DeviceModel declared, bool towel_mode_has_value) |
| Compare a heater's declared model with whether it answered the towel-dryer mode register (0x0136) with a value. | |
| std::string | format_heater_model_name (const uint8_t *bytes, size_t len) |
| Render a heater's model-name register (0x0119, ASCII) for a log line. | |
| bool | pairing_node_has_heater_at_next_address (DeviceType type, uint8_t manufacturer) |
| Does a pairing node that answered discovery this way control a heater at the next address? | |
| bool | next_node_id (const uint8_t node[NODE_ID_SIZE], uint8_t next[NODE_ID_SIZE]) |
| The node address one above node, as a 24-bit number (B6BDFB -> B6BDFC). | |
| RainLimitationRule | rain_limitation_rule (const RainLimitationInput &in) |
| Decide which rain-limitation evidence, if any, a status reply provides. | |
| bool | is_rain_limited (const RainLimitationInput &in) |
| True when rain_limitation_rule() finds evidence of a rain limitation. | |
| const char * | rain_limitation_rule_name (RainLimitationRule rule) |
| Name of a rain-limitation rule for log lines. | |
| RainSensorState | rain_state_from_minimum_limit (uint16_t value_raw) |
| Rain state from the raw value of a minimum-limitation reply. | |
| RainSensorState | rain_state_from_limitation_reply (const LimitationStatus &status) |
| Rain state from a decoded limitation reply: the one place that knows which reply fields decide. | |
| bool | is_executing (ExecutionState state) |
| True when a device in this state is moving or about to: waiting for its turn (2), waiting for power (3) or executing (4). | |
| bool | reads_stopped_by_status_byte (uint8_t status_byte) |
| Does byte 0 of a status reply say the device is at rest? | |
| StoppedReport | classify_stopped_report (const StoppedReportInput &in) |
| Decide whether a "stopped" report means at rest. | |
| bool | stopped_report_at_rest (StoppedReport report) |
| True when a verdict means the device is at rest. | |
| bool | stopped_report_keeps_candidate (StoppedReport report) |
| True when a verdict leaves an off-target report to remember for the next one. | |
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. | |
| constexpr uint32_t | HEATING_POLL_TIMER_ID_TAG = 0x02000000 |
| Namespace tag for heating_poll_timer_id(): the same node-address scheme under its own bit, so a heater's regular-cadence timer and a remote-activity poll timer for the same device never replace one another. | |
| constexpr uint8_t | HEATER_MODEL_CHECK_MAX_RUNS = 3 |
| Runs of the one-off heater model check before the hub gives up on it for this boot. | |
| constexpr uint8_t | MODEL_NAME_PRINTABLE_FIRST = 0x20 |
| First and last byte format_heater_model_name() prints as itself: printable ASCII, space to '~'. | |
| constexpr uint8_t | MODEL_NAME_PRINTABLE_LAST = 0x7E |
| See MODEL_NAME_PRINTABLE_FIRST. | |
| static constexpr uint8_t | RAIN_MINIMUM_LIMIT_PERCENT = 89 |
| A window with a wired rain sensor limits its own opening while it rains. | |
| static constexpr uint16_t | POSITION_FULL_PERCENT = 100 |
| Percent value of a fully limited position, the divisor of the raw position coding. | |
| static constexpr uint16_t | POSITION_RAW_PER_PERCENT = STATUS_POS_MAX / POSITION_FULL_PERCENT |
| Raw position units per percent (512). | |
| static constexpr uint16_t | RAIN_MINIMUM_LIMIT_RAW = POSITION_RAW_PER_PERCENT * RAIN_MINIMUM_LIMIT_PERCENT |
| RAIN_MINIMUM_LIMIT_PERCENT in the raw position coding (0xB200). | |
|
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 37 of file hub_decisions.h.
|
strong |
Disposition for the first response in an authenticated exchange.
Definition at line 30 of file hub_decisions.h.
|
strong |
What the model check suggests about a heater's declared io_model:.
Definition at line 915 of file hub_decisions.h.
|
strong |
Disposition for a candidate reply to a discovery-confirm request (0x2C).
Definition at line 56 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 43 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 50 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 66 of file hub_decisions.h.
|
strong |
Which evidence path labelled a device as limited by rain.
Definition at line 997 of file hub_decisions.h.
|
strong |
What a request is for, which decides how its attempts are spent.
| Enumerator | |
|---|---|
| PROGRESS | A look at a device that is probably still busy (a mid-travel poll): one attempt, the backoff ladder is the retry. |
| SETTLING | A read that decides where a device ended up: as many attempts as fit the budget. |
| COMMAND | A request that is harmless to repeat (is_repeat_safe_command()): to a mains device as many attempts as fit the budget, to a low-power one today's few with a gap. |
Definition at line 437 of file hub_decisions.h.
|
strong |
What the hub makes of one status report's movement state.
Definition at line 1114 of file hub_decisions.h.
|
strong |
Whether a low-power receiver is believed to be travelling right now, judged from what this hub has seen of it.
Orders the tries of a directed exchange: a moving receiver hears the short start preamble and ignores the long wake-up one, a resting one needs the long one.
Having merely heard from the device recently is deliberately not a reason to lead short: a resting VELUX SSL solar roller shutter ignored the short preamble on every try in field logs, including tries sent 34 ms after it had answered (ADR 0040, amendment).
| Enumerator | |
|---|---|
| ASLEEP | Not known to be moving — lead with the wake-up preamble. |
| AWAKE | Moving, or about to be stopped — the short preamble is the one it hears. |
| UNKNOWN | A status read to a receiver the hub has no recent news of (no moving evidence, and either no status report within LOW_POWER_MAX_TRAVEL_MS — boot, a status refresh — or a read its caller marked with AttemptRequest::with_state_unknown(), such as a catch-up read): it may be travelling, when it ignores the wake-up preamble, or at rest, when it hears only that one. Every try of a single-try read keeps the wake-up preamble; a settling read leads with one short attempt of its own (AttemptPlan::short_lead) and then sends the wake-up attempts.
|
Definition at line 698 of file hub_decisions.h.
|
inlinenodiscard |
The attempt budget rule: an attempt starts only if the whole attempt, up to the end of the last wait it can make, ends inside budget_ms.
Both the engine's guard (ExchangeEngine::try_fits_budget_()) and the plan that counts attempts (attempts_that_fit()) use it, so a plan never promises an attempt the guard would refuse for a target that stays silent. The attempt's overhead (EXCHANGE_ATTEMPT_OVERHEAD_MS) is counted here and nowhere else.
| elapsed_ms | Time since the exchange began. |
| gap_ms | Wait before the attempt is sent. |
| tx_ms | The attempt's transmit time as the radio estimates it. |
| window_ms | How long the attempt then listens for the first reply. |
| auth_tail_ms | The final-reply window a challenged attempt adds, 0 until the target has challenged. |
| budget_ms | exchange_total_budget_ms. |
Definition at line 501 of file hub_decisions.h.
|
inlinenodiscard |
The attempt plan for a request, from what it is for and who it asks.
A settling read, and a repeat-safe command to a mains device, repeat without a gap: every attempt listens from the end of its own transmission, so a silent attempt is already followed by the next one as soon as its window ends. The attempts before the last listen only exchange_early_response_wait_ms (a mains device answers within tens of milliseconds or not at all) and the last listens the whole window, so how many fit is attempts_that_fit(), capped at EXCHANGE_MAX_ATTEMPTS. A settling read of a low-power device makes LOW_POWER_SETTLING_ATTEMPTS attempts of LOW_POWER_RESPONSE_WAIT_MS each. The same rule the engine's guard applies at run time, so a plan never promises an attempt the guard would refuse when the target stays silent. A challenged attempt costs more (the final reply window), so the guard can still stop a plan early.
PROGRESS and a COMMAND to a low-power device keep today's shapes: one attempt, and EXCHANGE_RETRY_COUNT attempts with the default gap, both with the tuned start window.
| kind | What the request is for. |
| low_power | The target is a low-power receiver (YAML low_power, CTRL1 low-power bit). |
| tuning | Reply windows and exchange budget. |
| attempt_tx_ms | One attempt's transmit time as the radio estimates it (long wake-up preamble for a low-power target); only a gapless plan uses it. |
| last_window_ms | Listen time of the last attempt of a mains target, or of every attempt of a gapped plan; 0 for the tuned start window. |
| short_lead_tx_ms | Set to lead a settling read of a low-power target with one short attempt (the receiver's state is unknown, see WakeBelief::UNKNOWN): that attempt's transmit time. The lead is added to the wake-up attempts the budget admits, which are the same as without it; when all of them do not fit with the lead, the exchange may run past the budget by the lead's cost (AttemptPlan::budget_allowance_ms). No lead when only one wake-up attempt fits, or for any other read. |
Definition at line 571 of file hub_decisions.h.
|
inlinenodiscard |
The plan for a caller that names only a number of attempts: that many (at least one, at most EXCHANGE_RETRY_COUNT), the default gap, and window_ms.
| max_tries | Attempts the caller asks for. |
| window_ms | First-reply window of every attempt. |
Definition at line 535 of file hub_decisions.h.
|
inlinenodiscard |
How many attempts to a silent target fit the budget when none waits a gap, capped at cap.
The first n - 1 attempts listen early_window_ms and the last listens last_window_ms, and the last must still pass attempt_fits(), so n is the largest with (n - 1) * (tx + overhead + early) + tx + overhead + last < budget. At least one attempt: the first always runs. A zero budget is no limit the plan can honour (the engine's guard still applies), so it yields cap.
| budget_ms | exchange_total_budget_ms. |
| attempt_tx_ms | One attempt's transmit time as the radio estimates it. |
| early_window_ms | Listen time of every attempt but the last. |
| last_window_ms | Listen time of the last attempt. |
| cap | Most attempts the caller allows. |
Definition at line 519 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 166 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 147 of file hub_decisions.h.
|
inline |
Compare a heater's declared model with whether it answered the towel-dryer mode register (0x0136) with a value.
Two Atlantic Adelis towel dryers answer that register with their mode; an AGILIA radiator and a heater with a night light have not been read there yet, so the hint is worded "looks like" and never changes behaviour. The caller only asks after the device answered the read (with or without a value).
| declared | The YAML model. |
| towel_mode_has_value | True when the register's reply carried a value byte. |
Definition at line 929 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 275 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 234 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 253 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 295 of file hub_decisions.h.
|
inlinenodiscard |
Decide whether a "stopped" report means at rest.
A cover can come to rest off its target and report STOPPED there (corpus captures named on StopCandidate). The device's flag is trusted once two reports agree on the same target and the same current position; the first such report keeps the device "moving" for one more poll. Comparing the target as well keeps a device safe that answers "stopped" with a newly commanded target before its motor starts: the report before the command had the old target, so it can never confirm. With no follow-up poll to confirm by, the device's flag stands, or the device would read moving with nothing left to correct it.
Known bound: a device that answers "stopped, new target, old position" on two polls in a row before its motor starts (a start delay longer than one settle poll, about 3 s) is taken as at rest and polling ends. No captured device does this.
| in | The report and the hub's memory of the previous one. |
Definition at line 1152 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 background poll (OperationQueue::is_background_op()). |
| 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 393 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 320 of file hub_decisions.h.
|
inline |
Render a heater's model-name register (0x0119, ASCII) for a log line.
Stops at the first NUL, turns any other non-printable byte into '?', and trims trailing spaces, so a padded or garbled reply still logs as one readable line.
| bytes | The value bytes of the reply. |
| len | Their number. |
Definition at line 947 of file hub_decisions.h.
|
inline |
Whether a frame issue (a frame the hub did not act on) is logged at WARN rather than DEBUG.
WARN only when the frame is addressed to a registered device or to the hub itself: those are exchanges the hub is part of, so an unhandled or rejected frame is worth a look. A frame from a registered device to a broadcast address or to another controller is traffic the hub merely overheard (a heater's menu beacon, a gateway polling the same device), so it stays at DEBUG however well the source is known.
| dst_registered | Frame destination is a registered device. |
| dst_is_us | Frame destination is the hub's own node ID. |
Definition at line 856 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 131 of file hub_decisions.h.
|
inline |
Check if two frames have identical src/dst node IDs.
Definition at line 124 of file hub_decisions.h.
|
inline |
Numeric set_timeout() id for a heater's regular-cadence poll timer (IOHomeControlComponent::schedule_heating_poll_()).
Same keying as remote_poll_timer_id(), in the HEATING_POLL_TIMER_ID_TAG namespace, so a poll that a linked remote triggers cannot cancel it.
| node_id | 3-byte device node address. |
Definition at line 904 of file hub_decisions.h.
|
inline |
Returns true for a frame that reports a device event rather than a status: a CMD_SET_CONFIG1_RESP, or a well-formed CMD_STATUS_UPDATE whose event is not EVENT_STATUS_CHANGE (a subscribed sensor report, see decode_status_update_event()).
Such a frame is recorded on the "Last Device Event" sensor and never decoded as a position, whether it arrives on its own or inside an exchange.
Definition at line 87 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 355 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 81 of file hub_decisions.h.
|
inlinenodiscard |
True when a device in this state is moving or about to: waiting for its turn (2), waiting for power (3) or executing (4).
A queued cover is part of the move that follows, so it is followed like a running one. 0 (unable to execute), 1 (error during the last move) and 5 (done) are at rest. UNDEFINED is not a state; the caller falls back on the stopped bit (reads_stopped_by_status_byte()).
| state | State read from byte 0 of a status reply (execution_state_of()). |
Definition at line 1094 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 820 of file hub_decisions.h.
|
inlinenodiscard |
True when rain_limitation_rule() finds evidence of a rain limitation.
Definition at line 1033 of file hub_decisions.h.
|
inlinenodiscard |
Whether a request may be sent again to a device that said nothing, without a second action.
A device that hears a request acts on it and answers; silence therefore means it did not hear, or its reply was lost. For a request whose copies have one effect (an absolute position, open, close, STOP, a read) repeating it is harmless either way. CMD_EXECUTE with POS_FAVORITE is not (is_repeatable_execute()): on a Somfy motor it stops a moving cover, so a second copy can undo the first. Every request the hub sends that is not a CMD_EXECUTE is idempotent.
| request | Outbound request frame. |
Definition at line 613 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, vent, partial and pedestrian (every NAMED_POSITIONS entry): 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 178 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 748 of file hub_decisions.h.
|
inline |
Returns true when response is the answer to the subscription request: the request is a CMD_SUBSCRIBE_EVENT with the subscribe action, and the response a well-formed CMD_STATUS_UPDATE carrying the same event, from the device the request went to.
A device answers a subscription with the event's current value (tests/corpus/captures/probe/somfy_rs100_probe_subscribe_event_*), so inside the exchange the hub started for that request this event report is the reply; every other event report is still recorded and never taken as a reply (is_device_event_report()).
Definition at line 100 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, wake = wake_preamble): AWAKE short/wake/wake, ASLEEP and UNKNOWN wake on every try (the short lead of an UNKNOWN settling read is an attempt of its own, AttemptPlan::short_lead). A moving receiver answers the first short try; when it does not, it has most likely come to rest, where only the wake-up preamble reaches it, so every later try uses that one. A single-try exchange (most scheduler-owned status polls) sends only try 1's preamble; its backoff ladder, and the evidence a silent AWAKE exchange spends (ExchangeEngine::set_wake_evidence_spent_handler()), cover 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 moving (normal_start_preamble). |
| wake_preamble | Wake-up preamble for a resting receiver (low_power_wake_preamble). |
Definition at line 787 of file hub_decisions.h.
|
inlinenodiscard |
True while the remaining time the receiver last reported has not run out: by its own account it is still travelling, so a silent exchange is not evidence that it has come to rest.
False when no remaining time is known (0). Wrap-safe.
| evidence | Stamps for the target device. |
| now | Current millis(). |
Definition at line 741 of file hub_decisions.h.
|
inline |
The node address one above node, as a 24-bit number (B6BDFB -> B6BDFC).
A carry into the higher bytes (xxxxFF) is computed as plain arithmetic; no radiator with such an address has been seen, so whether the device counts the same way is unconfirmed.
| node | Address to count from. | |
| [out] | next | The next address; untouched when false is returned. |
Definition at line 981 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 868 of file hub_decisions.h.
|
inline |
Returns true for the commands that write to a device: an execute, a mode activation or a private register write.
A heard write changes the device's state, so this hub's view of it is stale; a heard read (any other command) changes nothing on the device. Deliberately a short list of writes rather than a list of reads, so an unknown command counts as a read.
Definition at line 117 of file hub_decisions.h.
|
inline |
Does a pairing node that answered discovery this way control a heater at the next address?
An Atlantic radiator in its display's pairing mode answers discovery as a remote controller (node type 1023), hands over its key, and is controlled at the next node address up: three AGILIA IO radiators follow this (issue #165, tests/corpus/captures/pairing/atlantic_agilia_pairing_key_pull_heater_rollcall.yaml). The heater answers a roll-call only now and then, so the pairing asks that address directly first.
| type | Node type the pairing node answered discovery with. |
| manufacturer | Manufacturer byte of that discovery reply (0 when the reply carried none). |
Definition at line 970 of file hub_decisions.h.
|
inlinenodiscard |
Decide which rain-limitation evidence, if any, a status reply provides.
A rain limitation is an inference from observations, never a prediction (ADR 0030 keeps predictions apart): windows clamp an open to their ventilation position without sending CMD_ERROR_RESP / RESULT_LIMITATION_BY_RAIN, so the only evidence is what the device reports in its ordinary status replies. Rule order matters: the originator rule is the direct evidence and wins; the clamp rule only applies while earlier rain evidence is still fresh.
| in | What the reply said and what had been predicted. |
Definition at line 1023 of file hub_decisions.h.
|
inlinenodiscard |
Name of a rain-limitation rule for log lines.
Definition at line 1038 of file hub_decisions.h.
|
inlinenodiscard |
Rain state from a decoded limitation reply: the one place that knows which reply fields decide.
Only the main parameter's value counts; originator and remaining time do not.
| status | A decoded CMD_LIMITATION_STATUS_RESP payload. |
Definition at line 1079 of file hub_decisions.h.
|
inlinenodiscard |
Rain state from the raw value of a minimum-limitation reply.
| value_raw | Big-endian raw value in position coding (LimitationStatus::value_raw). |
Definition at line 1069 of file hub_decisions.h.
|
inlinenodiscard |
Does byte 0 of a status reply say the device is at rest?
The execution state decides (is_executing()). A byte whose bits [2:0] hold 6 or 7 has no state, and its STATUS_STOPPED bit decides as it did before the state was read.
| status_byte | Byte 0 of the reply's payload. |
Definition at line 1104 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 894 of file hub_decisions.h.
|
inlinenodiscard |
Whether an authenticated-but-unanswered request may be sent again.
Every request except CMD_EXECUTE and CMD_SUBSCRIBE_EVENT 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.
CMD_SUBSCRIBE_EVENT stores a subscription in the device, and the device answers it with the event's current value (is_subscription_answer()). A try the device authenticated has stored the subscription, but its answer may still have been lost, so it is sent once more (UNCONFIRMED_SUBSCRIBE_MAX_RESENDS), never more: the device has accepted repeated subscriptions (issue #156), and a reading is what the request is for.
| 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 210 of file hub_decisions.h.
|
inlinenodiscard |
True when a scheduler-owned status poll sits in the middle band of the backoff ladder.
See SCHEDULED_POLL_RETRY_GRACE_FIRST_FAILURE (proto_timing.h) for why the band is where a poll is worth more than one attempt: the slot that lands just after a manoeuvre ends, rather than the start or the tail of the ladder.
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.
| status_poll_failures | Consecutive silent failures already recorded for this device. |
| auth_poll_failures | Consecutive challenge-seen failures already recorded. |
Definition at line 416 of file hub_decisions.h.
|
inline |
Transmit-attempt budget for a scheduler-owned status poll of a heater, by ladder position.
Heaters keep this count-based budget; a cover's polls take their plan from scheduled_poll_read_kind() instead.
| status_poll_failures | Consecutive silent failures already recorded for this device. |
| auth_poll_failures | Consecutive challenge-seen failures already recorded. |
Definition at line 429 of file hub_decisions.h.
|
inlinenodiscard |
What a cover's status poll is for, from where it sits and what it settles.
A read that decides where the cover ended up is a SETTLING read: the poll after an accepted STOP, the one confirming a cover that stopped short of its target (settles_a_position), and the polls in the ladder's retry band (scheduled_poll_in_retry_band()). Every other scheduled poll is a look at a device that is probably still busy or unreachable (PROGRESS): the ladder is its retry.
| status_poll_failures | Consecutive silent failures already recorded for this device. |
| auth_poll_failures | Consecutive challenge-seen failures already recorded. |
| settles_a_position | True for the poll after an accepted STOP or one confirming a stop off target (StatusPollPolicy::take_settling_read()). |
Definition at line 629 of file hub_decisions.h.
|
inlinenodiscard |
What the short lead attempt costs the exchange: its transmission, its brief listen and the attempt overhead (the same terms attempt_fits() counts for any attempt).
| short_tx_ms | The lead's transmit time as the radio estimates it (short start preamble). |
Definition at line 483 of file hub_decisions.h.
|
inline |
The order in which a 0x72 status-update acknowledgement is sent on the three channels: the channel the 0x71 exchange ran on first, then the other two in their usual CH1, CH2, CH3 order.
The device is still listening on the channel its exchange used, so the copy that matters is not left to the end of the sequence. A frequency that is not one of the three channels gives the usual order.
| receive_freq_hz | Frequency the 0x71 frame (and the challenge exchange) arrived on. |
Definition at line 835 of file hub_decisions.h.
|
inlinenodiscard |
True when a verdict means the device is at rest.
Definition at line 1167 of file hub_decisions.h.
|
inlinenodiscard |
True when a verdict leaves an off-target report to remember for the next one.
Kept after a confirmation too, so a later poll that finds the device still resting at the same place confirms at once instead of settling again.
Definition at line 1175 of file hub_decisions.h.
|
inlinenodiscard |
Build the exchange engine's view of a device record.
| dev | Device record to read. |
Definition at line 731 of file hub_decisions.h.
|
inlinenodiscard |
Judge whether a low-power receiver is travelling.
AWAKE for a STOP request or while moving evidence is younger than LOW_POWER_MAX_TRAVEL_MS. For a status read with neither moving evidence nor a status report within that time the answer is UNKNOWN; 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()). |
| is_status_read | True when the request is a status read (CMD_PRIVATE): only a read can be UNKNOWN, a command keeps the rule it had. |
| state_unknown | True when the caller marked the read as made without trustworthy news (AttemptRequest::with_state_unknown()): a recent status report then does not make the belief ASLEEP. Only a status read takes it; fresh moving evidence still wins. |
Definition at line 765 of file hub_decisions.h.
|
inlinenodiscard |
Lowercase name of a belief for log lines.
Definition at line 796 of file hub_decisions.h.
|
constexpr |
Runs of the one-off heater model check before the hub gives up on it for this boot.
Each run costs at most two register reads, and runs only on a poll the device answered.
Definition at line 912 of file hub_decisions.h.
|
constexpr |
Namespace tag for heating_poll_timer_id(): the same node-address scheme under its own bit, so a heater's regular-cadence timer and a remote-activity poll timer for the same device never replace one another.
Definition at line 881 of file hub_decisions.h.
|
constexpr |
First and last byte format_heater_model_name() prints as itself: printable ASCII, space to '~'.
Definition at line 938 of file hub_decisions.h.
|
constexpr |
See MODEL_NAME_PRINTABLE_FIRST.
Definition at line 939 of file hub_decisions.h.
|
staticconstexpr |
Percent value of a fully limited position, the divisor of the raw position coding.
Definition at line 1057 of file hub_decisions.h.
|
staticconstexpr |
Raw position units per percent (512).
Definition at line 1059 of file hub_decisions.h.
|
staticconstexpr |
A window with a wired rain sensor limits its own opening while it rains.
Home Assistant's VELUX integration reads that limit and calls it rain from this minimum limit upward (observed values 89, 91, 93 and 100 %).
Definition at line 1055 of file hub_decisions.h.
|
staticconstexpr |
RAIN_MINIMUM_LIMIT_PERCENT in the raw position coding (0xB200).
Definition at line 1063 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.
Each numeric-id timer caller has its own tag (see HEATING_POLL_TIMER_ID_TAG), so two timers for the same device never replace one another and an id says which caller it belongs to.
Definition at line 876 of file hub_decisions.h.