Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
esphome::home_io_control::PairingEngine Class Reference

Owns and drives all three phases of the IO-Homecontrol device pairing flow. More...

#include <pairing_engine.h>

Collaboration diagram for esphome::home_io_control::PairingEngine:

Public Member Functions

 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.
 PairingEngine (const PairingEngine &)=delete
 Non-copyable — stores double-pointer and references into hub member addresses.
PairingEngineoperator= (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.

Detailed Description

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.

Constructor & Destructor Documentation

◆ PairingEngine() [1/2]

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.

Parameters
radio_ptrDouble pointer into the hub's radio_ member — survives driver replacement in tests.
node_idController 3-byte node ID buffer, owned by the hub.
system_key16-byte AES system key buffer, owned by the hub.
tuningTuning configuration, owned by the hub.
engineShared exchange engine for transmit/receive operations.
registryDevice registry where paired devices are permanently registered.
telemetryPer-attempt telemetry recorder, owned by the hub.
recent_oneway_sightingMost 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.

◆ PairingEngine() [2/2]

esphome::home_io_control::PairingEngine::PairingEngine ( const PairingEngine & )
delete

Non-copyable — stores double-pointer and references into hub member addresses.

Here is the call graph for this function:

Member Function Documentation

◆ discover_and_pair()

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.

Returns
true if all three phases completed successfully; false otherwise.

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.

Here is the call graph for this function:

◆ finalize_pairing_configuration_()

bool esphome::home_io_control::PairingEngine::finalize_pairing_configuration_ ( pairing::PairingContext & context)
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.

Parameters
contextPairing context with device information from phases 1 and 2.
Returns
true if the SetConfig1 exchange completed; false if it was skipped or failed.

Definition at line 460 of file pairing_engine.cpp.

Here is the call graph for this function:

◆ operator=()

PairingEngine & esphome::home_io_control::PairingEngine::operator= ( const PairingEngine & )
delete
Here is the call graph for this function:

◆ parse_device_from_discovery()

DiscoveryResponseInfo esphome::home_io_control::PairingEngine::parse_device_from_discovery ( const IoFrame & frame,
IoDevice & device,
std::string & device_id )
static

Extract node ID, device type, and subtype from a CMD_DISCOVER_RESP frame.

Parse a discovery response frame into device metadata and ID.

Returns
The decoded extended discovery fields (manufacturer / Multi Information Byte / length flags), so a caller can read the self-reported power class without decoding twice.

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.

Here is the call graph for this function:

◆ run_discovery_phase_()

decisions::PairingDiscoveryDisposition esphome::home_io_control::PairingEngine::run_discovery_phase_ ( pairing::PairingContext & context)
protected

Phase 1: broadcast discovery command(s) and wait for a device response (0x29).

Parameters
contextPairing context updated on success.
Returns
ACCEPT on success; NO_RESPONSE or INVALID otherwise.

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.

Here is the call graph for this function:

◆ run_key_exchange_phase_()

bool esphome::home_io_control::PairingEngine::run_key_exchange_phase_ ( pairing::PairingContext & context)
protected

Phase 2: authenticated key exchange (0x31 → 0x3C → 0x32 → 0x33).

Parameters
contextPairing context populated by run_discovery_phase_().
Returns
true if key exchange completes; false on any failure.

Steps:

  1. Transmit CMD_KEY_INIT (0x31)
  2. Wait for device challenge (0x3C)
  3. Transmit CMD_KEY_TRANSFER (0x32) with encrypted system key
  4. Wait for CMD_KEY_CONFIRM (0x33)

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:

  • The device replies with CMD_KEY_CONFIRM (0x33) directly — it already had the key from the first 0x32 and is just auto-confirming again. Nothing more to send.
  • The device replies with a fresh CMD_CHALLENGE_REQ (0x3C) instead — proof it never received the first 0x32 at all (a device that already holds the key doesn't re-challenge), so there is no key transfer for it to confirm yet. The loop calls transfer_key_and_wait_confirm_() again here, replaying CMD_KEY_TRANSFER against this new challenge, rather than discarding the 0x3C and burning the retry for nothing — the device's next 0x31 would just produce another fresh challenge either way, so retrying without resending 0x32 could never succeed.

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.

Here is the call graph for this function:

◆ transfer_key_and_wait_confirm_()

bool esphome::home_io_control::PairingEngine::transfer_key_and_wait_confirm_ ( pairing::PairingContext & context)
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.

Parameters
contextPairing context; context.rx.data supplies the challenge bytes, context.req is filled with the outbound 0x32, context.resp with the inbound 0x33 on success.
Returns
true if the device confirmed the key.

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.

Here is the call graph for this function:

◆ wait_for_discovery_response_()

decisions::PairingDiscoveryDisposition esphome::home_io_control::PairingEngine::wait_for_discovery_response_ ( uint32_t timeout_ms,
RadioRxPacket & packet,
IoFrame & response_frame )
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.

Parameters
timeout_msMaximum wait window.
packetOutput: raw RadioRxPacket of the accepted discovery frame.
response_frameOutput: parsed IoFrame of the accepted discovery frame.
Returns
ACCEPT on success; NO_RESPONSE (no traffic) or INVALID (wrong frames) otherwise.

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.

Here is the call graph for this function:

◆ wait_for_key_challenge_()

bool esphome::home_io_control::PairingEngine::wait_for_key_challenge_ ( uint32_t timeout_ms,
RadioRxPacket & packet,
IoFrame & challenge_frame,
const uint8_t device_node_id[NODE_ID_SIZE] )
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.

Parameters
timeout_msMaximum wait window.
packetOutput: raw RadioRxPacket of the accepted frame.
challenge_frameOutput: parsed IoFrame.
device_node_idExpected source node ID (devices paired to).
Returns
true if a valid challenge or confirm was received; false on timeout.

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.

Here is the call graph for this function:

◆ wait_for_key_confirm_()

bool esphome::home_io_control::PairingEngine::wait_for_key_confirm_ ( pairing::PairingContext & context)
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.

Here is the call graph for this function:

The documentation for this class was generated from the following files: