Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
pairing_engine.h
Go to the documentation of this file.
1#pragma once
2
3/// @file pairing_engine.h
4/// @brief Device discovery and key-exchange engine for IO-Homecontrol pairing.
5/// @ingroup hioc_hub
6///
7/// PairingEngine encapsulates all three phases of the IO-Homecontrol pairing flow:
8///
9/// Phase 1 — Discovery (0x28 → 0x29):
10/// Controller broadcasts a discovery packet. A device in pairing mode responds
11/// with its node ID and type/subtype metadata.
12///
13/// Phase 2 — Authenticated Key Exchange (0x31 → 0x3C → 0x32 → 0x33):
14/// The controller sends CMD_KEY_INIT (0x31). The device challenges with 0x3C.
15/// The controller proves knowledge of the system key and simultaneously
16/// transfers the encrypted system key (0x32). The device confirms with 0x33.
17///
18/// Phase 3 — Configuration (0x6F):
19/// The controller sends SetConfig1 to enable automatic device status updates.
20///
21/// The engine is constructed once by IOHomeControlComponent. It holds double-pointer
22/// indirection for the radio driver so test assignments (`comp.radio_ = &mock`) propagate
23/// without calling setup(). All radio operations go through ExchangeEngine so that
24/// LBT, preamble selection, and frequency hopping are centralised.
25///
26/// PairingEngine is non-copyable and non-movable because it stores pointer and reference
27/// addresses that would dangle in a copy.
28
29#include "proto_frame.h"
30#include "proto_codecs.h"
31#include "hub_pairing.h"
32#include "hub_decisions.h"
33#include "exchange_engine.h"
34#include "device_registry.h"
35#include "pairing_advisor.h"
36#include "pairing_telemetry.h"
37#include "radio_interface.h"
38#include "tuning_config.h"
39
40#include <cstdint>
41#include <string>
42
43namespace esphome {
44namespace home_io_control {
45
46/// @name Pairing timing constants
47/// Timeouts and retry limits for the pairing flow's blocking waits.
48///@{
49inline constexpr uint32_t PAIRING_DISCOVERY_RESPONSE_TIMEOUT_MS = 2000; ///< Discovery wait window after sending 0x28.
50inline constexpr uint8_t PAIRING_DISCOVERY_MAX_ATTEMPTS = 3; ///< Retry discovery TX up to this many times.
51inline constexpr uint32_t PAIRING_KEY_CHALLENGE_TIMEOUT_MS = 500; ///< Wait window for the device's 0x3C challenge.
52inline constexpr uint32_t PAIRING_KEY_CONFIRM_TIMEOUT_MS = 500; ///< Wait for 0x33 key confirm after sending 0x32.
53/// How recent a RecentOneWayPairingSighting has to be, relative to discover_and_pair() starting,
54/// to still count as evidence for this attempt. Generous relative to the doc's "a few seconds"
55/// PROG-then-press guidance: real field reports (issue #27) show gaps up to ~4-7 s between the
56/// PROG gesture and pressing "Discover & Pair" in the app, so a tight window would reintroduce
57/// the same miss it's meant to fix. Not tied to ONEWAY_QUIET_PERIOD_MS (status_poll_policy.h,
58/// 700 ms) — that constant is about collapsing one remote's repeat burst, a different, much
59/// shorter timescale than "how long ago did the user press PROG."
60inline constexpr uint32_t PAIRING_RECENT_ONE_WAY_SIGHTING_WINDOW_MS = 15000;
61///@}
62
63/// Owns and drives all three phases of the IO-Homecontrol device pairing flow.
64///
65/// Constructed once by IOHomeControlComponent; collaborators (radio, exchange engine,
66/// device registry) are injected as pointers/references so the engine never outlives them.
67/// @ingroup hioc_hub
69 public:
70 /// Construct the engine with all required collaborators.
71 ///
72 /// @param radio_ptr Double pointer into the hub's `radio_` member — survives driver replacement in tests.
73 /// @param node_id Controller 3-byte node ID buffer, owned by the hub.
74 /// @param system_key 16-byte AES system key buffer, owned by the hub.
75 /// @param tuning Tuning configuration, owned by the hub.
76 /// @param engine Shared exchange engine for transmit/receive operations.
77 /// @param registry Device registry where paired devices are permanently registered.
78 /// @param telemetry Per-attempt telemetry recorder, owned by the hub.
79 /// @param recent_oneway_sighting Most recent 1W pairing-gesture sighting from the hub's normal
80 /// passive RX path, owned by the hub; see RecentOneWayPairingSighting.
81 PairingEngine(RadioDriver **radio_ptr, const uint8_t *node_id, const uint8_t *system_key, const TuningConfig *tuning,
82 ExchangeEngine &engine, DeviceRegistry &registry, PairingTelemetry &telemetry,
83 const RecentOneWayPairingSighting &recent_oneway_sighting);
84
85 /// Non-copyable — stores double-pointer and references into hub member addresses.
86 PairingEngine(const PairingEngine &) = delete;
88
89 /// Discover and pair a device currently in pairing mode (three-phase orchestrator).
90 /// @return true if all three phases completed successfully; false otherwise.
91 bool discover_and_pair();
92
93 /// Extract node ID, device type, and subtype from a CMD_DISCOVER_RESP frame.
94 /// @return The decoded extended discovery fields (manufacturer / Multi Information Byte / length
95 /// flags), so a caller can read the self-reported power class without decoding twice.
97 std::string &device_id);
98
99 protected:
100 // --- Phase helpers (protected; exposed to tests via TestablePairingEngine in test_helpers.h) ---
101
102 /// Phase 1: broadcast discovery command(s) and wait for a device response (0x29).
103 /// @param context Pairing context updated on success.
104 /// @return ACCEPT on success; NO_RESPONSE or INVALID otherwise.
106
107 /// Phase 2: authenticated key exchange (0x31 → 0x3C → 0x32 → 0x33).
108 /// @param context Pairing context populated by run_discovery_phase_().
109 /// @return true if key exchange completes; false on any failure.
111
112 /// Phase 3: send SetConfig1 (0x6F) to enable automatic status updates; best-effort.
113 /// Pairing always proceeds regardless of the outcome — the return value is informational
114 /// only, used to distinguish PairingOutcome::PAIRED from PairingOutcome::CONFIG_FAILED in
115 /// telemetry; it never causes discover_and_pair() to report failure.
116 /// @param context Pairing context with device information from phases 1 and 2.
117 /// @return true if the SetConfig1 exchange completed; false if it was skipped or failed.
119
120 /// Wait for a discovery response (0x29) within timeout_ms with per-chip frequency hopping.
121 /// @param timeout_ms Maximum wait window.
122 /// @param packet Output: raw RadioRxPacket of the accepted discovery frame.
123 /// @param response_frame Output: parsed IoFrame of the accepted discovery frame.
124 /// @return ACCEPT on success; NO_RESPONSE (no traffic) or INVALID (wrong frames) otherwise.
126 IoFrame &response_frame);
127
128 /// Wait for a key-challenge (0x3C) or direct key-confirm (0x33) from the target device.
129 /// @param timeout_ms Maximum wait window.
130 /// @param packet Output: raw RadioRxPacket of the accepted frame.
131 /// @param challenge_frame Output: parsed IoFrame.
132 /// @param device_node_id Expected source node ID (devices paired to).
133 /// @return true if a valid challenge or confirm was received; false on timeout.
134 bool wait_for_key_challenge_(uint32_t timeout_ms, RadioRxPacket &packet, IoFrame &challenge_frame,
135 const uint8_t device_node_id[NODE_ID_SIZE]);
136
137 /// Transmit the 0x32 key transfer and wait for the 0x33 key confirm with retry.
139
140 /// Build CMD_KEY_TRANSFER against the current challenge and wait for the 0x33 confirm; see
141 /// run_key_exchange_phase_()'s doc comment for why this is a separate, replayable step.
142 /// @param context Pairing context; `context.rx.data` supplies the challenge bytes, `context.req`
143 /// is filled with the outbound 0x32, `context.resp` with the inbound 0x33 on success.
144 /// @return true if the device confirmed the key.
146
147 private:
148 /// Convenience accessor returning the current radio driver (dereferences double pointer).
149 [[nodiscard]] RadioDriver *radio_() const { return *radio_ptr_; }
150
151 /// Record the final outcome, detach telemetry from the exchange engine, and log the
152 /// end-of-attempt summary. Called once at every discover_and_pair() exit point.
153 /// @param outcome Final disposition of this attempt.
154 void finish_pairing_attempt_(PairingOutcome outcome);
155
156 /// Record an RX or RX_REJECT telemetry event for a discovery-response candidate frame.
157 /// Factored out of wait_for_discovery_response_() purely to keep that function's cognitive
158 /// complexity under the clang-tidy threshold — no behavior beyond the telemetry call.
159 /// @param frame Parsed candidate frame.
160 /// @param accepted true if the frame was classified as a valid discovery response.
161 /// @param rssi RSSI of the captured frame.
162 void record_discovery_rx_telemetry_(const IoFrame &frame, bool accepted, int16_t rssi);
163
164 RadioDriver **radio_ptr_;
165 const uint8_t *node_id_;
166 const uint8_t *system_key_;
167 const TuningConfig *tuning_;
168 ExchangeEngine &engine_;
169 DeviceRegistry &registry_;
170 PairingTelemetry &telemetry_;
171 const RecentOneWayPairingSighting &recent_oneway_sighting_;
172};
173
174} // namespace home_io_control
175} // namespace esphome
Owns the per-hub device table, update callbacks, and linked-remote associations.
decisions::PairingDiscoveryDisposition run_discovery_phase_(pairing::PairingContext &context)
Phase 1: broadcast discovery command(s) and wait for a device response (0x29).
static DiscoveryResponseInfo parse_device_from_discovery(const IoFrame &frame, IoDevice &device, std::string &device_id)
Extract node ID, device type, and subtype from a CMD_DISCOVER_RESP frame.
PairingEngine & operator=(const PairingEngine &)=delete
bool run_key_exchange_phase_(pairing::PairingContext &context)
Phase 2: authenticated key exchange (0x31 → 0x3C → 0x32 → 0x33).
bool wait_for_key_confirm_(pairing::PairingContext &context)
Transmit the 0x32 key transfer and wait for the 0x33 key confirm with retry.
PairingEngine(const PairingEngine &)=delete
Non-copyable — stores double-pointer and references into hub member addresses.
bool wait_for_key_challenge_(uint32_t timeout_ms, RadioRxPacket &packet, IoFrame &challenge_frame, const uint8_t device_node_id[NODE_ID_SIZE])
Wait for a key-challenge (0x3C) or direct key-confirm (0x33) from the target device.
bool discover_and_pair()
Discover and pair a device currently in pairing mode (three-phase orchestrator).
bool transfer_key_and_wait_confirm_(pairing::PairingContext &context)
Build CMD_KEY_TRANSFER against the current challenge and wait for the 0x33 confirm; see run_key_excha...
decisions::PairingDiscoveryDisposition wait_for_discovery_response_(uint32_t timeout_ms, RadioRxPacket &packet, IoFrame &response_frame)
Wait for a discovery response (0x29) within timeout_ms with per-chip frequency hopping.
bool finalize_pairing_configuration_(pairing::PairingContext &context)
Phase 3: send SetConfig1 (0x6F) to enable automatic status updates; best-effort.
PairingEngine(RadioDriver **radio_ptr, const uint8_t *node_id, const uint8_t *system_key, const TuningConfig *tuning, ExchangeEngine &engine, DeviceRegistry &registry, PairingTelemetry &telemetry, const RecentOneWayPairingSighting &recent_oneway_sighting)
Construct the engine with all required collaborators.
Fixed-size per-attempt telemetry recorder for the pairing flow.
Abstract radio driver for IO-Homecontrol.
Per-hub device table, update-callback fan-out, and linked-remote map.
Self-contained authenticated exchange engine for IO-Homecontrol 2W.
Pure transition helpers for hub-owned exchange and pairing frame decisions.
Internal pairing-state model for hub‑owned discovery and key‑exchange flows.
PairingDiscoveryDisposition
Disposition during pairing discovery phase.
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
Definition proto_sizes.h:20
constexpr uint8_t PAIRING_DISCOVERY_MAX_ATTEMPTS
Retry discovery TX up to this many times.
constexpr uint32_t PAIRING_KEY_CONFIRM_TIMEOUT_MS
Wait for 0x33 key confirm after sending 0x32.
PairingOutcome
Final disposition of a pairing attempt, used by the result sensor string.
constexpr uint32_t PAIRING_RECENT_ONE_WAY_SIGHTING_WINDOW_MS
How recent a RecentOneWayPairingSighting has to be, relative to discover_and_pair() starting,...
constexpr uint32_t PAIRING_KEY_CHALLENGE_TIMEOUT_MS
Wait window for the device's 0x3C challenge.
constexpr uint32_t PAIRING_DISCOVERY_RESPONSE_TIMEOUT_MS
Discovery wait window after sending 0x28.
Read-only advisor that turns PairingTelemetry into actionable diagnostics.
Structured per-attempt telemetry recorder for the pairing flow.
Device-name, address-classification and 1W-frame codecs.
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
Radio abstraction layer for IO-Homecontrol.
Extended discovery-response fields (manufacturer, Multi Information Byte, backbone address,...
Runtime state of a paired IO‑Homecontrol device.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:88
Raw packet received from the radio.
A 1W pairing-gesture frame observed on the hub's normal passive RX path, remembered so a fresh discov...
All runtime tunable parameters for pairing and radio diagnostics.
Context object that lives for the duration of a single pairing attempt.
Definition hub_pairing.h:59
Runtime tuning configuration for pairing and radio diagnostics.