|
Home IO Control
ESPHome add-on for IO-Homecontrol devices
|
Owns and drives all three phases of the IO-Homecontrol device pairing flow. More...
#include <pairing_engine.h>
Public Member Functions | |
| PairingEngine (RadioDriver **radio_ptr, const uint8_t *node_id, const uint8_t *system_key, const TuningConfig *tuning, ExchangeEngine &engine, DeviceRegistry ®istry, PairingTelemetry &telemetry, const RecentOneWayPairingSighting &recent_oneway_sighting) | |
| Construct the engine with all required collaborators. | |
| PairingEngine (const PairingEngine &)=delete | |
| Non-copyable — stores double-pointer and references into hub member addresses. | |
| PairingEngine & | operator= (const PairingEngine &)=delete |
| bool | discover_and_pair () |
| Discover and pair a device currently in pairing mode (three-phase orchestrator). | |
Static Public Member Functions | |
| 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. | |
Protected Member Functions | |
| decisions::PairingDiscoveryDisposition | run_discovery_phase_ (pairing::PairingContext &context) |
| Phase 1: broadcast discovery command(s) and wait for a device response (0x29). | |
| bool | run_key_exchange_phase_ (pairing::PairingContext &context) |
| Phase 2: authenticated key exchange (0x31 → 0x3C → 0x32 → 0x33). | |
| bool | finalize_pairing_configuration_ (pairing::PairingContext &context) |
| Phase 3: send SetConfig1 (0x6F) to enable automatic status updates; best-effort. | |
| 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 | 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 | wait_for_key_confirm_ (pairing::PairingContext &context) |
| Transmit the 0x32 key transfer and wait for the 0x33 key confirm with retry. | |
| 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_exchange_phase_()'s doc comment for why this is a separate, replayable step. | |
Owns and drives all three phases of the IO-Homecontrol device pairing flow.
Constructed once by IOHomeControlComponent; collaborators (radio, exchange engine, device registry) are injected as pointers/references so the engine never outlives them.
Definition at line 68 of file pairing_engine.h.
| esphome::home_io_control::PairingEngine::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.
| radio_ptr | Double pointer into the hub's radio_ member — survives driver replacement in tests. |
| node_id | Controller 3-byte node ID buffer, owned by the hub. |
| system_key | 16-byte AES system key buffer, owned by the hub. |
| tuning | Tuning configuration, owned by the hub. |
| engine | Shared exchange engine for transmit/receive operations. |
| registry | Device registry where paired devices are permanently registered. |
| telemetry | Per-attempt telemetry recorder, owned by the hub. |
| recent_oneway_sighting | Most recent 1W pairing-gesture sighting from the hub's normal passive RX path, owned by the hub; see RecentOneWayPairingSighting. |
Definition at line 52 of file pairing_engine.cpp.
|
delete |
Non-copyable — stores double-pointer and references into hub member addresses.
| bool esphome::home_io_control::PairingEngine::discover_and_pair | ( | ) |
Discover and pair a device currently in pairing mode (three-phase orchestrator).
Pairing orchestrator — high-level three-phase flow.
Phase 1: run_discovery_phase_() finds a device in pairing mode. Phase 2: run_key_exchange_phase_() performs authenticated key establishment. Phase 3: finalize_pairing_configuration_() sends SetConfig1 (best-effort).
On success the device is added to the registry and a YAML snippet is printed to the log. The hub's thin wrapper manages the busy_ flag before and after this call.
Definition at line 477 of file pairing_engine.cpp.
|
protected |
Phase 3: send SetConfig1 (0x6F) to enable automatic status updates; best-effort.
Phase 3: send SetConfig1 (0x6F) to enable automatic status updates. Best-effort.
Pairing always proceeds regardless of the outcome — the return value is informational only, used to distinguish PairingOutcome::PAIRED from PairingOutcome::CONFIG_FAILED in telemetry; it never causes discover_and_pair() to report failure.
| context | Pairing context with device information from phases 1 and 2. |
Definition at line 460 of file pairing_engine.cpp.
|
delete |
|
static |
Extract node ID, device type, and subtype from a CMD_DISCOVER_RESP frame.
Parse a discovery response frame into device metadata and ID.
Decodes node ID, device type, subtype, and the extended fields (manufacturer, backbone, Multi Information Byte) via decode_discovery_response(), then emits pairing's diagnostic log lines for whichever extended fields the payload actually included. The inversion flag is derived from the type via default_inverted_for_type().
Definition at line 271 of file pairing_engine.cpp.
|
protected |
Phase 1: broadcast discovery command(s) and wait for a device response (0x29).
| context | Pairing context updated on success. |
Sends each configured discovery command in order, waiting up to pairing_discovery_wait_ms for a valid response after each TX. Retries up to PAIRING_DISCOVERY_MAX_ATTEMPTS times per command.
Definition at line 310 of file pairing_engine.cpp.
|
protected |
Phase 2: authenticated key exchange (0x31 → 0x3C → 0x32 → 0x33).
| context | Pairing context populated by run_discovery_phase_(). |
Steps:
Step 4 depends on the driver's TX→RX turnaround: fast-turnaround radios await the 0x33 through the standard send_and_receive() exchange; slow-turnaround radios use the dedicated wait_for_key_confirm_() path with a key-init re-trigger, because the 0x33 would otherwise arrive while the receiver is still settling (see RadioDriver::has_fast_tx_rx_turnaround()).
The slow-turnaround re-trigger loop below (for (int re = 0; ...)) has two distinct outcomes for its re-sent CMD_KEY_INIT, and they are handled differently:
That replay roughly doubles this function's worst-case blocking time when every wait times out (approximately 3.5s -> 6.6s: two of the loop's iterations can now each wait out a full key-transfer-and-confirm cycle instead of returning immediately on a missed 0x33). This is a known, accepted trade-off: pairing already tolerates multi-second blocking exchanges (see EXCHANGE_TOTAL_BUDGET_MS's own reasoning, proto_timing.h), and the alternative — leaving a slow-turnaround device that never got its key stuck retrying forever — is worse than the occasional slower failure path.
Definition at line 398 of file pairing_engine.cpp.
|
protected |
Build CMD_KEY_TRANSFER against the current challenge and wait for the 0x33 confirm; see run_key_exchange_phase_()'s doc comment for why this is a separate, replayable step.
Build CMD_KEY_TRANSFER against the challenge currently held in context.rx.data and wait for the 0x33 confirm, routing through the fast- or slow-turnaround path.
| context | Pairing context; context.rx.data supplies the challenge bytes, context.req is filled with the outbound 0x32, context.resp with the inbound 0x33 on success. |
Shared by run_key_exchange_phase_()'s first attempt and its slow-turnaround retry loop, which calls this again against a freshly re-issued challenge (see that function's doc comment) rather than discarding it.
Definition at line 239 of file pairing_engine.cpp.
|
protected |
Wait for a discovery response (0x29) within timeout_ms with per-chip frequency hopping.
Wait for a valid discovery response (0x29) within timeout_ms.
| timeout_ms | Maximum wait window. |
| packet | Output: raw RadioRxPacket of the accepted discovery frame. |
| response_frame | Output: parsed IoFrame of the accepted discovery frame. |
Listens with per-chip frequency hopping between slices. Distinguishes between NO_RESPONSE (no packets at all) and INVALID (packets seen but none valid).
Frequency hopping: hops between the 2 non-request IO-homecontrol channels after each slice — the request always goes out on FREQ_CH2 (see run_discovery_phase_()), and a broadcast reply essentially never lands back on that channel (see ListenPolicy's own doc comment for why), so dwelling there is wasted listening time. Same policy as collect_broadcast_responses() uses for its own broadcast wait (spec.request_freq below). The slice length comes from RadioDriver::hop_dwell_ms() (spec.dwell_ms left at 0, so listen() asks the driver). When preamble or sync detection fires, the dwell extends by PREAMBLE_LINGER_DWELL_MS so the incoming frame can complete without interruption.
Definition at line 79 of file pairing_engine.cpp.
|
protected |
Wait for a key-challenge (0x3C) or direct key-confirm (0x33) from the target device.
Wait for a key-challenge (0x3C) or direct key-confirm (0x33) from target device.
| timeout_ms | Maximum wait window. |
| packet | Output: raw RadioRxPacket of the accepted frame. |
| challenge_frame | Output: parsed IoFrame. |
| device_node_id | Expected source node ID (devices paired to). |
During key exchange the device typically responds to 0x31 with a random 6-byte challenge (0x3C). Some devices skip the challenge and send 0x33 directly — indicating immediate key acceptance (observed mostly when the controller's TX→RX turnaround is slow enough that the 0x3C is missed). Both are accepted.
Uses ExchangeEngine::listen() with ListenPolicy::HOLD_REQUEST_CHANNEL: this is a unicast reply to a unicast request (the 0x31 key-init), and every measured unicast pairing reply came back on the request channel, so there is nothing here to hop for — same reasoning as wait_for_key_confirm_(). This loop runs on all three chips (it is called before the has_fast_tx_rx_turnaround() branch in run_key_exchange_phase_()), unlike the dedicated confirm wait, which only slow-turnaround radios reach.
Definition at line 124 of file pairing_engine.cpp.
|
protected |
Transmit the 0x32 key transfer and wait for the 0x33 key confirm with retry.
Transmit the 0x32 key transfer and wait for 0x33 key confirm (with retry).
Only reached on slow-turnaround radios (RadioDriver::has_fast_tx_rx_turnaround() == false, i.e. SX1262/LR1121): fast-turnaround radios (SX1276) catch the 0x33 through the standard ExchangeEngine::send_and_receive_() / wait_for_first_response_() path instead and never call this function — see run_key_exchange_phase_(). Uses ExchangeEngine::listen() with ListenPolicy::HOLD_REQUEST_CHANNEL (does not hop, does not slice, see below) and the driver's response_preamble() (drivers whose TX waveform needs more lock-on margin return a longer preamble). Retries up to EXCHANGE_RETRY_COUNT times on timeout.
Definition at line 166 of file pairing_engine.cpp.