Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
key_extraction_responder.h
Go to the documentation of this file.
1#pragma once
2
3/// @file key_extraction_responder.h
4/// @brief "Recover System Key" (key extraction) — device-role responder collaborator.
5/// @ingroup hioc_hub
6///
7/// The impure side of the key-extraction feature: arming/disarming, throwaway node-ID generation,
8/// the 10-minute auto-off timer, the post-extraction grace window, transmitting device-role
9/// replies, and the security-sensitive result log block. The pure state-transition decisions live
10/// in pairing_responder.h/.cpp (unchanged); this collaborator owns a
11/// pairing_responder::ResponderContext and dispatches the six RX branches through
12/// try_handle_frame(), called from process_received_packet_() (hub_status.cpp).
13
14#include "hub_hooks.h"
15#include "pairing_responder.h"
16#include "proto_frame.h"
17
18#include "esphome/core/hal.h" // millis() for the inline awaiting_reply()
19
20#include <cstdint>
21#include <functional>
22
23namespace esphome {
24namespace home_io_control {
25
26// Forward declarations — full definitions included only in key_extraction_responder.cpp.
27struct TuningConfig;
28class RadioDriver;
29class DeviceRegistry;
30
31/// @brief Device-role responder for the "Recover System Key" feature.
32///
33/// Constructed once by IOHomeControlComponent; non-copyable because it is wired with injected
34/// callbacks and references into hub member addresses (mirrors ManagementActions / ExchangeEngine).
35/// @ingroup hioc_hub
37 public:
38 /// @param node_id Hub's real 3-byte node ID (throwaway-ID collision check).
39 /// @param radio Double pointer to the hub's active radio driver, so a test's
40 /// `comp.radio_ = &mock` propagates (mirrors ExchangeEngine).
41 /// @param tuning Runtime tuning config, owned by the hub
42 /// (`cold_broadcast_reply_preamble`).
43 /// @param registry Device registry, for the throwaway-ID collision check only.
44 /// @param transmit How to put a reply frame on air (see TransmitFrameFn).
45 /// @param schedule_auto_off Named-timeout scheduler for the 10-minute arm window and the
46 /// post-extraction grace window (see NamedTimeoutFn).
47 KeyExtractionResponder(const uint8_t *node_id, RadioDriver **radio, const TuningConfig *tuning,
48 DeviceRegistry &registry, TransmitFrameFn transmit, NamedTimeoutFn schedule_auto_off);
49
50 /// Non-copyable — holds injected callbacks and references into hub member addresses.
53
54 /// @brief Arm or disarm the "Recover System Key" (key extraction) responder.
55 ///
56 /// Arming picks a fresh throwaway node ID, resets the pairing_responder state machine to
57 /// ARMED_IDLE, and schedules a 10-minute auto-off. While armed, the 0x28/0x2C/0x31/0x32 branches
58 /// in process_received_packet_() emulate an unpaired device so a user's existing hub can pair to
59 /// it and hand over its node_id/system_key (see pairing_responder.h). Disarming — manual, via
60 /// the HA switch, on successful extraction, or on auto-off — immediately stops those branches
61 /// from responding; it never touches the real device registry or the hub's own node_id_/
62 /// system_key_. This is the body that was IOHomeControlComponent::set_key_extraction_armed().
63 /// @param armed Desired state.
64 void set_armed(bool armed);
65
66 /// Register a callback invoked whenever the key-extraction armed state changes — manual
67 /// toggle, successful extraction, or auto-off timeout — so the switch entity can keep its
68 /// displayed state in sync when the responder disarms itself rather than the user. Single-slot.
69 /// @param cb Callable receiving the new armed state.
70 void set_armed_callback(std::function<void(bool)> cb) { this->armed_callback_ = std::move(cb); }
71
72 /// Dispatch a frame to the responder if it's one of its 0x28/0x2C/0x31/0x32/0x36/0x3C frames and
73 /// the responder is armed. Kept a separate function (rather than inlined into
74 /// process_received_packet_()) purely to keep that function's cognitive complexity under the
75 /// clang-tidy threshold, mirroring PairingEngine::record_discovery_rx_telemetry_()'s reason for
76 /// existing.
77 /// @param frame Parsed inbound frame.
78 /// @return true if the frame was handled (caller should stop further dispatch for it).
79 [[nodiscard]] bool try_handle_frame(const IoFrame &frame);
80
81 /// Generate a random throwaway node ID for one key-extraction arm cycle, avoiding collisions
82 /// with the broadcast addresses, this hub's own real node ID, and any registered device.
83 /// @param out Output: 3-byte node ID.
84 void generate_throwaway_id(uint8_t out[NODE_ID_SIZE]);
85
86 /// (Re)arm the post-extraction grace window that replaces the old immediate disarm-on-extraction:
87 /// called once when the key is first recovered, and again on every sign of hub progress after
88 /// that (an inbound 0x36, an outbound 0x3D) so a slow multi-retry hub isn't cut off mid-round.
89 /// Uses the same named-timer replace-on-reschedule idiom as the 10-minute auto-off timer — see
90 /// key_extraction_responder.cpp for why a naive "only disarm if DISARMED" guard inside the
91 /// callback is not enough once a manual disarm-and-rearm can happen inside the window.
93
94 /// True whenever the responder has replied at least once, is waiting on the hub's next step, and
95 /// that wait is still within its bounded hold window — i.e. `key_extraction_ctx_.state` is
96 /// neither DISARMED (feature unused) nor ARMED_IDLE (armed, but no discovery request seen yet),
97 /// AND `key_extraction_hold_deadline_ms_` has not yet passed. loop() uses this to hold CH2
98 /// instead of running the generic idle-hop scan, and to defer background status polls, while an
99 /// attempt is in flight.
100 ///
101 /// The deadline is a plain timestamp, not a named `set_timeout()` timer: releasing the CH2 hold
102 /// is purely a radio-scheduling optimization (see key_extraction_hold_deadline_ms_'s own doc
103 /// comment for why it is deliberately decoupled from `key_extraction_ctx_.state` itself), so
104 /// nothing needs to fire a callback when it lapses — the next loop() iteration simply stops
105 /// taking the CH2-hold branch on its own. Default-constructed, the deadline is 0, which is always
106 /// in the past relative to any real `millis()` reading once the device has been running — so a
107 /// mid-exchange state reached without the deadline having been (re)set (e.g. a reply-builder
108 /// failure between the state guard and the deadline update) safely never holds CH2, rather than
109 /// holding it unboundedly.
110 ///
111 /// Defined inline: defer_background_poll_() calls this every loop() iteration.
117
118 /// State for the current "Accept Foreign Pairing" (key-extraction) arm cycle; DISARMED by
119 /// default so a fresh boot never responds to foreign pairing traffic. See pairing_responder.h.
120 /// Public so the host tests that script individual RX branches can preset and inspect it.
122 /// Deadline (millis()) until which loop() should hold CH2 for the key-extraction responder,
123 /// deliberately independent of `key_extraction_ctx_.state` itself. Set (not "armed" — this is a
124 /// plain timestamp, not a named `set_timeout()` timer) on every sign of hub progress: the three
125 /// pre-extraction reply handlers (handle_discover_(), handle_discover_confirm_(),
126 /// handle_key_init_(), key_extraction_responder.cpp) push it out by
127 /// KEY_EXTRACTION_MID_ATTEMPT_TIMEOUT_MS, and arm_post_extraction_grace() pushes it out by
128 /// KEY_EXTRACTION_POST_EXTRACT_GRACE_MS so the hold also covers the (much longer)
129 /// post-extraction address-verification phase it governs.
130 ///
131 /// Deliberately independent of `key_extraction_ctx_.state`: the pure guards in
132 /// pairing_responder.cpp decide whether an inbound frame is accepted by checking `state` alone,
133 /// never this deadline, so a real (slower) hub's next frame still completes the exchange
134 /// correctly even if it arrives after the hold has expired. Coupling the two — letting the
135 /// deadline also force `state` back to ARMED_IDLE — would silently discard that live protocol
136 /// progress instead of just releasing the radio hold. See awaiting_reply()'s doc comment for how
137 /// this is consumed.
139
140 private:
141 /// Handle an inbound CMD_DISCOVER_REQ (0x28) while the responder is armed.
142 void handle_discover_(const IoFrame &frame);
143 /// Handle an inbound CMD_DISCOVER_CONFIRM (0x2C) addressed to our throwaway node ID while armed.
144 void handle_discover_confirm_(const IoFrame &frame);
145 /// Handle an inbound CMD_KEY_INIT (0x31) addressed to our throwaway node ID while armed.
146 void handle_key_init_(const IoFrame &frame);
147 /// Handle an inbound CMD_KEY_TRANSFER (0x32) addressed to our throwaway node ID while armed.
148 void handle_key_transfer_(const IoFrame &frame);
149 /// Handle an inbound CMD_ADDRESS_REQ (0x36) addressed to our throwaway node ID while armed.
150 /// Some hubs (Velux KLR200) send this after completing the key exchange, to verify the backbone
151 /// address they were given — see pairing_responder::on_address_req().
152 void handle_address_req_(const IoFrame &frame);
153 /// Handle an inbound CMD_CHALLENGE_REQ (0x3C) addressed to our throwaway node ID while armed and
154 /// in SENT_ADDRESS_RESP — the hub-issued challenge against our own CMD_ADDRESS_RESP, closing the
155 /// address-verification round a CMD_ADDRESS_REQ opened. See
156 /// pairing_responder::on_address_challenge().
157 void handle_address_challenge_(const IoFrame &frame);
158 /// Transmit a key-extraction reply frame on all 3 IO-homecontrol channels, using the radio
159 /// driver's response_preamble() rather than a fixed SHORT_PREAMBLE/LONG_PREAMBLE constant —
160 /// long enough that a channel-hopping receiver reliably lands on it, short enough that 3
161 /// sequential transmissions don't block the main loop for the better part of a second (see the
162 /// implementation comment in key_extraction_responder.cpp for the hardware-confirmed reasoning).
163 /// Shared by every RX handler so the preamble choice and channel list are defined once.
164 void broadcast_reply_(const IoFrame &frame);
165 /// Emit the security-sensitive "system key extracted" log block (see redaction.h — this is the
166 /// one deliberate, explicit exception to that file's masking, not a loosening of it).
167 void log_result_();
168
169 const uint8_t *node_id_;
170 RadioDriver **radio_;
171 const TuningConfig *tuning_;
172 DeviceRegistry &registry_;
173 TransmitFrameFn transmit_;
174 NamedTimeoutFn schedule_auto_off_;
175 std::function<void(bool)> armed_callback_;
176};
177
178} // namespace home_io_control
179} // namespace esphome
Owns the per-hub device table, update callbacks, and linked-remote associations.
KeyExtractionResponder & operator=(const KeyExtractionResponder &)=delete
void generate_throwaway_id(uint8_t out[NODE_ID_SIZE])
Generate a random throwaway node ID for one key-extraction arm cycle, avoiding collisions with the br...
KeyExtractionResponder(const KeyExtractionResponder &)=delete
Non-copyable — holds injected callbacks and references into hub member addresses.
void set_armed(bool armed)
Arm or disarm the "Recover System Key" (key extraction) responder.
void arm_post_extraction_grace()
(Re)arm the post-extraction grace window that replaces the old immediate disarm-on-extraction: called...
pairing_responder::ResponderContext key_extraction_ctx_
State for the current "Accept Foreign Pairing" (key-extraction) arm cycle; DISARMED by default so a f...
bool try_handle_frame(const IoFrame &frame)
Dispatch a frame to the responder if it's one of its 0x28/0x2C/0x31/0x32/0x36/0x3C frames and the res...
void set_armed_callback(std::function< void(bool)> cb)
Register a callback invoked whenever the key-extraction armed state changes — manual toggle,...
bool awaiting_reply() const
True whenever the responder has replied at least once, is waiting on the hub's next step,...
KeyExtractionResponder(const uint8_t *node_id, RadioDriver **radio, const TuningConfig *tuning, DeviceRegistry &registry, TransmitFrameFn transmit, NamedTimeoutFn schedule_auto_off)
uint32_t key_extraction_hold_deadline_ms_
Deadline (millis()) until which loop() should hold CH2 for the key-extraction responder,...
Abstract radio driver for IO-Homecontrol.
Injected-capability callback aliases shared by the hub's collaborator objects.
@ ARMED_IDLE
Armed, listening for a discovery request (0x28).
@ DISARMED
Not armed; 0x28/0x2C/0x31/0x32 traffic is ignored.
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
Definition proto_sizes.h:20
std::function< bool(const IoFrame &frame, uint32_t freq_hz, uint16_t preamble)> TransmitFrameFn
Puts a frame on air on a given channel via the hub's protected transmit_frame_().
Definition hub_hooks.h:34
std::function< void(const char *name, uint32_t delay_ms, std::function< void()> callback)> NamedTimeoutFn
Schedules a named, replace-on-same-name timeout on the hub's ESPHome scheduler.
Definition hub_hooks.h:27
Pure decision logic for the device-role "Accept Foreign Pairing" (system-key extraction) responder.
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:88
All runtime tunable parameters for pairing and radio diagnostics.