18#include "esphome/core/application.h"
19#include "esphome/core/log.h"
30const char *
const TAG =
"home_io_control";
33bool frame_is_key_confirm(
const IoFrame &frame) {
return frame.cmd == CMD_KEY_CONFIRM; }
39 ESP_LOGW(
TAG,
"No device responded to discovery");
42 ESP_LOGW(
TAG,
"No valid discovery response received");
53void delay_feeding_wdt(uint32_t ms) {
56 const uint32_t chunk = ms < 1000 ? ms : 1000;
69 : radio_ptr_(radio_ptr),
71 system_key_(system_key),
75 telemetry_(telemetry),
76 recent_oneway_sighting_(recent_oneway_sighting) {}
78uint16_t PairingEngine::pairing_start_preamble_(
const IoFrame &frame)
const {
115 spec.
on_hop = [
this]() { this->telemetry_.record_hop(); };
117 bool saw_traffic =
false;
119 engine_.listen(spec, packet, response_frame, [&](
const IoFrame *parsed,
const RadioRxPacket & ) {
121 if (parsed ==
nullptr)
125 this->record_discovery_rx_telemetry_(*parsed, accepted, radio_()->get_last_capture().rssi_dbm);
149 const uint8_t device_node_id[NODE_ID_SIZE]) {
154 bool saw_traffic =
false;
156 engine_.listen(spec, packet, challenge_frame, [&](
const IoFrame *parsed,
const RadioRxPacket & ) {
158 if (parsed ==
nullptr)
160 const int16_t rssi = radio_()->get_last_capture().rssi_dbm;
161 if (parsed->
cmd == CMD_KEY_CONFIRM && memcmp(parsed->
src, device_node_id, NODE_ID_SIZE) == 0 &&
162 memcmp(parsed->
dst, node_id_, NODE_ID_SIZE) == 0) {
163 this->telemetry_.record_rx(*parsed, rssi);
168 this->telemetry_.record_rx_reject(*parsed, rssi);
171 this->telemetry_.record_rx(*parsed, rssi);
177 ESP_LOGW(TAG, saw_traffic ?
"Key exchange: no valid challenge received" :
"Key exchange: no challenge received");
198 spec.
on_hop = [
this]() { this->telemetry_.record_hop(); };
206 if (parsed == nullptr)
207 return ReplyDisposition::IGNORE;
208 const int16_t rssi = radio_()->get_last_capture().rssi_dbm;
209 disposition = decisions::classify_pairing_discover_confirm_reply(*parsed, context.device.node_id, node_id_);
210 if (disposition == decisions::PairingDiscoverConfirmDisposition::IGNORE) {
211 this->telemetry_.record_rx_reject(*parsed, rssi);
212 return ReplyDisposition::IGNORE;
214 this->telemetry_.record_rx(*parsed, rssi);
235decisions::PairingKeyConfirmDisposition PairingEngine::listen_for_key_confirm_(pairing::PairingContext &context,
237 bool after_challenge) {
239 spec.window_ms = PAIRING_KEY_CONFIRM_TIMEOUT_MS;
240 spec.policy = ListenPolicy::HOLD_REQUEST_CHANNEL;
242 bool saw_any =
false;
243 auto disposition = decisions::PairingKeyConfirmDisposition::IGNORE;
245 engine_.listen(spec, context.packet, context.resp, [&](
const IoFrame *parsed,
const RadioRxPacket &packet) {
247 ESP_LOGD(TAG,
"Key confirm wait: got %u bytes on freq=%" PRIu32, packet.len, packet.freq_hz);
248 if (parsed == nullptr) {
249 ESP_LOGD(TAG,
"Key confirm wait: parse failed");
250 return ReplyDisposition::IGNORE;
252 ESP_LOGD(TAG,
"Key confirm wait: parsed cmd=0x%02X src=%02X%02X%02X dst=%02X%02X%02X", parsed->cmd,
253 parsed->src[0], parsed->src[1], parsed->src[2], parsed->dst[0], parsed->dst[1], parsed->dst[2]);
254 disposition = decisions::classify_pairing_key_confirm_reply(context.req, *parsed);
255 if (disposition == decisions::PairingKeyConfirmDisposition::IGNORE)
256 return ReplyDisposition::IGNORE;
258 const int16_t rssi = radio_()->get_last_capture().rssi_dbm;
259 if (disposition == decisions::PairingKeyConfirmDisposition::REFUSE) {
260 this->telemetry_.record_rx_reject(*parsed, rssi);
261 ESP_LOGW(TAG,
"Key transfer: device responded with cmd=%s(0x%02X) (expected KEY_CONFIRM 0x33)",
262 command_name(parsed->cmd), parsed->cmd);
263 if (parsed->cmd == CMD_ERROR_RESP && parsed->data_len > 0)
264 ESP_LOGW(TAG,
"Key transfer: error code=0x%02X", parsed->data[0]);
265 return ReplyDisposition::ABORT;
270 this->telemetry_.record_rx(*parsed, rssi);
271 return disposition == decisions::PairingKeyConfirmDisposition::CONFIRM ? ReplyDisposition::ACCEPT
272 : ReplyDisposition::ABORT;
275 if (outcome == ListenOutcome::TIMED_OUT) {
276 ESP_LOGI(TAG,
"Try %u%s: no response for key transfer (0x32) within %" PRIu32
" ms (saw_any=%d)", try_number,
277 after_challenge ?
" (post-challenge)" :
"", PAIRING_KEY_CONFIRM_TIMEOUT_MS, saw_any);
278 return decisions::PairingKeyConfirmDisposition::IGNORE;
292 for (uint8_t tries = 0; tries < EXCHANGE_RETRY_COUNT; tries++) {
295 delay(EXCHANGE_RETRY_DELAY_MS);
297 if (!engine_.transmit_frame(context.
req, FREQ_CH2, radio_()->response_preamble()))
300 const uint8_t try_number = tries + 1;
303 ESP_LOGI(TAG,
"Key transfer: device challenged the key transfer, answering 0x3D");
309 if (engine_.answer_challenge(context.
req, context.
resp, FREQ_CH2))
310 disposition = listen_for_key_confirm_(context, try_number,
true);
331 this->telemetry_.set_phase(context.
state);
339 this->telemetry_.set_phase(context.
state);
343 if (radio_()->has_fast_tx_rx_turnaround()) {
347 frame_is_key_confirm(context.
resp);
361 std::string &device_id) {
364 if (frame.
data_len > DISCOVERY_RESP_MANUFACTURER_OFFSET) {
366 ESP_LOGI(TAG,
"Discovery: device %s manufacturer=%u (%s)", device_id.c_str(), info.
manufacturer, mfr_name);
369 "Unknown manufacturer ID %u reported by device %s. "
370 "Please file a GitHub issue with this ID and your device model so support can be added.",
374 if (frame.
data_len > DISCOVERY_RESP_BACKBONE_OFFSET + NODE_ID_SIZE - 1) {
377 if (frame.
data_len > DISCOVERY_RESP_FLAGS_OFFSET) {
380 ESP_LOGI(TAG,
"Discovery: device %s turnaround=%s power_save=%s flags=0x%02X", device_id.c_str(),
382 if (power_save == POWER_SAVE_LOW_POWER) {
384 "Device %s reports low-power mode: add 'low_power: true' to its YAML entry. "
385 "The pairing snippet below pre-fills it when one is printed.",
400 if (tuning_->pairing_discovery_initial_dwell_ms > 0) {
401 ESP_LOGD(TAG,
"Discovery: initial dwell %u ms", tuning_->pairing_discovery_initial_dwell_ms);
402 delay(tuning_->pairing_discovery_initial_dwell_ms);
409 bool saw_invalid =
false;
411 for (
size_t command_index = 0; command_index < tuning_->pairing_discovery_commands.size(); ++command_index) {
412 auto command =
static_cast<uint8_t
>(tuning_->pairing_discovery_commands[command_index]);
414 tuning_->pairing_discovery_destination.data());
415 ESP_LOGD(TAG,
"Discovery command %zu/%zu: cmd=0x%02X dst=%02X%02X%02X", command_index + 1,
416 tuning_->pairing_discovery_commands.size(), command, destination[0], destination[1], destination[2]);
419 this->telemetry_.increment_discovery_attempt();
422 this->telemetry_.set_phase(context.
state);
424 tuning_->pairing_discovery_ack_capable, tuning_->pairing_discovery_payload_enabled,
425 tuning_->pairing_discovery_payload, system_key_) ||
426 !engine_.transmit_frame(context.
req, FREQ_CH2, tuning_->pairing_discovery_preamble)) {
432 this->telemetry_.set_phase(context.
state);
446 ESP_LOGI(TAG,
"Discovery attempt %u/%u for cmd=0x%02X: no response, retrying...", attempt,
479 this->telemetry_.set_phase(context.
state);
481 bool wait_phase_started =
false;
489 !engine_.transmit_frame(context.
req, FREQ_CH2, pairing_start_preamble_(context.
req))) {
494 if (!wait_phase_started) {
495 this->telemetry_.set_phase(context.
state);
496 wait_phase_started =
true;
500 const uint32_t wait_start_ms = millis();
504 ESP_LOGI(TAG,
"Discover confirm: 0x2D from %s on freq=%" PRIu32
" after %" PRIu32
" ms (try %u/%u)",
511 const uint8_t error_code = context.
rx.
data_len > 0 ? context.
rx.
data[0] : 0;
512 ESP_LOGW(TAG,
"Discover confirm: device %s answered 0x2C with error 0x%02X, continuing with key exchange",
521 ESP_LOGI(TAG,
"Discover confirm: no 0x2D from %s after %u tries, continuing with key exchange",
527 if (tuning_->pairing_key_init_delay_ms > 0)
528 delay_feeding_wdt(tuning_->pairing_key_init_delay_ms);
568 this->telemetry_.set_phase(context.
state);
570 !engine_.transmit_frame(context.
key_init, FREQ_CH2, pairing_start_preamble_(context.
key_init))) {
576 this->telemetry_.set_phase(context.
state);
582 if (context.
rx.
cmd == CMD_KEY_CONFIRM) {
583 ESP_LOGI(TAG,
"Device accepted key immediately (0x33 without 0x32 exchange)");
584 context.
resp = context.
rx;
591 ESP_LOGI(TAG,
"Challenge (0x3C) received: data_len=%u freq=%" PRIu32
" rssi=%d", context.
rx.
data_len,
592 context.
packet.
freq_hz, radio_()->get_last_capture().rssi_dbm);
598 if (!radio_()->has_fast_tx_rx_turnaround()) {
599 for (
int re = 0; !key_ok && re < 2; re++) {
600 ESP_LOGI(TAG,
"Key confirm missed, re-sending key-init to trigger auto-confirm (attempt %d/2)", re + 1);
602 delay(EXCHANGE_RETRY_DELAY_MS);
603 if (!engine_.transmit_frame(context.
key_init, FREQ_CH2, pairing_start_preamble_(context.
key_init)))
608 if (context.
rx.
cmd == CMD_KEY_CONFIRM) {
620 ESP_LOGW(TAG,
"Key exchange failed");
632 if (engine_.send_and_receive(context.
req, context.
resp, FREQ_CH2, PAIRING_SET_CONFIG1_MAX_TRIES,
634 ESP_LOGI(TAG,
"Pairing: no reply from %s to the optional SetConfig1 (0x6F); the pairing is complete",
650 this->telemetry_.begin();
651 this->engine_.set_transmit_observer(&this->telemetry_);
658 if (this->recent_oneway_sighting_.seen_ms != 0 &&
660 this->telemetry_.record_recent_one_way_sighting(this->recent_oneway_sighting_);
663 ESP_LOGI(TAG,
"Starting device discovery...");
671 log_discovery_diagnostic(disc_disp);
678 if (tuning_->pairing_discovery_preamble < LONG_PREAMBLE) {
679 ESP_LOGI(TAG,
"Pairing: directed frames to %s use the %u-byte discovery preamble it just answered",
680 context.
device_id.c_str(), tuning_->pairing_discovery_preamble);
688 bool key_exchanged =
false;
689 for (uint8_t ke_attempt = 0; ke_attempt < tuning_->pairing_key_exchange_retries; ke_attempt++) {
690 if (ke_attempt > 0) {
691 ESP_LOGI(TAG,
"Retrying key exchange (attempt %d/%u)...", ke_attempt + 1, tuning_->pairing_key_exchange_retries);
693 delay(EXCHANGE_RETRY_DELAY_MS);
696 key_exchanged =
true;
700 if (!key_exchanged) {
710 this->telemetry_.set_phase(context.
state);
724 if (snippet.empty()) {
729 "Device %s paired successfully, but this repo does not yet expose an ESPHome platform for type=%s "
730 "class=%s subtype=%u.",
734 "No ready-to-paste YAML was generated. If you want to experiment manually, choose the most likely "
735 "platform and set io_device_type: %s.",
737 ESP_LOGW(TAG,
"Please file a GitHub issue with this device type, subtype, model, and the pairing log so support "
742 this->telemetry_.set_phase(context.
state);
749 "Device %s paired successfully, but the discovery response did not include type/subtype "
750 "metadata, so the platform (cover/light/switch/lock) can't be determined automatically.",
752 ESP_LOGI(TAG,
"Add this to your YAML once you know what kind of device it is:\n%s", snippet.c_str());
753 ESP_LOGW(TAG,
"Please file a GitHub issue with the pairing log and device model so this discovery edge case can "
758 this->telemetry_.set_phase(context.
state);
763 ESP_LOGI(TAG,
"Device %s paired successfully! Add this to your YAML:\n%s", context.
device_id.c_str(),
768 "This snippet uses the raw device type %s because the project does not yet expose a named YAML alias "
770 type_yaml.c_str(), type_diag.c_str());
771 ESP_LOGW(TAG,
"Please file a GitHub issue with this type, subtype, device model, and the pairing log so support "
777 this->telemetry_.set_phase(context.
state);
782void PairingEngine::finish_pairing_attempt_(
PairingOutcome outcome) {
783 this->telemetry_.set_outcome(outcome);
784 this->engine_.set_transmit_observer(
nullptr);
785 this->telemetry_.log_summary();
789 std::string advice_codes;
790 for (uint8_t i = 0; i < advice_count; i++) {
792 if (!advice_codes.empty())
796 this->telemetry_.set_advice_codes(advice_codes);
799void PairingEngine::record_discovery_rx_telemetry_(
const IoFrame &frame,
bool accepted, int16_t rssi) {
801 this->telemetry_.record_rx(frame, rssi);
803 this->telemetry_.record_rx_reject(frame, rssi);
Owns the per-hub device table, update callbacks, and linked-remote associations.
uint16_t request_preamble_for(const IoFrame &request) const
Preamble length for an outbound request frame.
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.
decisions::PairingDiscoverConfirmDisposition wait_for_discover_confirm_ack_(uint8_t try_index, pairing::PairingContext &context)
Wait for one discover-confirm (0x2C) try's answer: a matching 0x2D, a matching CMD_ERROR_RESP,...
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.
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.
pairing::DiscoverConfirmResult run_discover_confirm_step_(pairing::PairingContext &context)
Discover-confirm step (0x2C → 0x2D): sent directly to the just-discovered device, once per discover_a...
void finalize_pairing_configuration_(pairing::PairingContext &context)
Phase 3: send SetConfig1 (0x6F) once; optional.
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.
Fixed-size per-attempt telemetry recorder for the pairing flow.
Abstract radio driver for IO-Homecontrol.
Pure transition helpers for hub-owned exchange and pairing frame decisions.
std::string pairing_advice_message(const PairingAdvice &advice)
const char * pairing_advice_code_name(PairingAdviceCode code)
uint8_t analyze_pairing_telemetry(const PairingTelemetry &telemetry, const uint8_t own_node_id[NODE_ID_SIZE], PairingAdvice out[PAIRING_ADVICE_MAX])
Inspect a completed pairing attempt's telemetry and produce actionable advice.
PairingDiscoveryDisposition
Disposition during pairing discovery phase.
@ ACCEPT
Valid discovery response received.
@ NO_RESPONSE
No packets received on the channel within timeout.
@ INVALID
Packets seen but none were valid discovery (0x29) frames.
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.
@ ACCEPT
Valid 0x3C challenge from target device.
PairingKeyConfirmDisposition
Disposition for a candidate reply to the key-transfer (0x32) confirm wait — the slow-turnaround path'...
@ REFUSE
Anything else (including CMD_ERROR_RESP) — an explicit or implicit refusal; ends the whole wait,...
@ CHALLENGE
A fresh CMD_CHALLENGE_REQ (0x3C) — the device may challenge the key transfer before confirming it; an...
@ CONFIRM
CMD_KEY_CONFIRM (0x33) — the device accepted the key.
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.
PairingDiscoverConfirmDisposition
Disposition for a candidate reply to a discovery-confirm request (0x2C).
@ ACK
CMD_DISCOVER_CONFIRM_ACK (0x2D) from the device — it wants to proceed.
@ IGNORE
Not from/to the expected endpoints, or a frame the step does not act on (e.g.
@ ERROR
CMD_ERROR_RESP from the device — an explicit, named refusal.
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 ...
DiscoverConfirmResult
What PairingEngine::run_discover_confirm_step_() actually observed.
@ ERROR_REPLY
The device answered with CMD_ERROR_RESP.
@ SKIPPED
pairing_discover_confirm tuning mode is skip — no 0x2C was sent.
@ NO_REPLY
Every try was silent (or every transmit failed).
@ ACKED
The device answered with CMD_DISCOVER_CONFIRM_ACK (0x2D).
@ REGISTER_DEVICE
Registering device in the runtime registry for the current boot.
@ TX_DISCOVER
Discovery broadcast (0x28) sent; awaiting device response.
@ WAIT_DISCOVER_RESPONSE
Listening for discovery response (0x29) from a device in pairing mode.
@ COMPLETE
Pairing completed successfully; device ready for use.
@ TX_DISCOVER_CONFIRM
Discovery-confirm (0x2C) sent to the discovered device.
@ WAIT_KEY_CHALLENGE
Waiting for challenge (0x3C) from device as part of key transfer.
@ WAIT_KEY_CONFIRM
Waiting for key‑confirm (0x33) from device (key receipt acknowledgement).
@ TX_KEY_INIT
Key‑init (0x31) sent to the discovered device.
@ TX_KEY_TRANSFER
Key‑transfer (0x32) sent with encrypted system key.
@ WAIT_DISCOVER_CONFIRM
Listening for the device's discovery-confirm ack (0x2D).
const char * manufacturer_name(uint8_t id)
Get a human-readable manufacturer name from the protocol manufacturer byte.
uint8_t discovery_power_save_mode(uint8_t flags)
Extract the power save mode field from a discovery response's Multi Information Byte.
@ ALL
CH1->CH2->CH3, including the request channel.
std::string format_device_type_for_yaml(DeviceType type)
Build the YAML value for a device's io_device_type key.
const char * att_class_name(uint8_t att_class)
Get a human-readable turnaround time string for an ATT class value.
constexpr uint8_t PAIRING_DISCOVERY_MAX_ATTEMPTS
Retry discovery TX up to this many times.
static constexpr const char * TAG
const char * power_save_mode_name(uint8_t mode)
Get a human-readable power save mode name.
bool create_discover_confirm(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, bool ack)
Build a discovery-confirm request (0x2C) — controller side, sent directly to a freshly-discovered dev...
PairingOutcome
Final disposition of a pairing attempt, used by the result sensor string.
@ INVALID_RESPONSE
Discovery saw traffic but nothing valid.
@ KEY_EXCHANGE_FAILED
Discovery succeeded but the key exchange did not complete.
@ PAIRED
The key exchange completed and the device is registered.
@ NO_RESPONSE
No device responded to discovery.
@ ACCEPT
This is the frame the caller was waiting for — stop listening, return ACCEPTED.
@ IGNORE
Unparsable / not ours / wrong exchange — keep listening.
bool create_set_config1(IoFrame &f, const uint8_t *own, const uint8_t *dst)
Build a set-config command (0x6F) to tell the device to automatically send status updates when contro...
bool create_key_init(IoFrame &f, const uint8_t *own, const uint8_t *dst)
Build a key-init request (0x31) to start the pairing key exchange with a discovered device.
@ SUCCESS_WITH_RESPONSE
Device replied; the caller's response frame is populated.
@ FAILED
No usable reply; the device may never have heard the request.
constexpr uint32_t PAIRING_DISCOVER_CONFIRM_ACK_TIMEOUT_MS
Wait window per discover-confirm try.
const char * pairing_stage_name(pairing::PairingState state)
Get a short, log/telemetry-friendly name for a pairing state.
constexpr uint8_t PAIRING_DISCOVER_CONFIRM_TRIES
Max tries for the discover-confirm (0x2C) step.
std::string tuning_config_full_snapshot(const TuningConfig &cfg)
Format the current tuning configuration as a full one-line snapshot.
@ ROTATE_SKIPPING_REQUEST
The two channels that are not the request channel.
@ ROTATE_ALL_CHANNELS
CH1->CH2->CH3->CH1.
@ HOLD_REQUEST_CHANNEL
Never retunes, never slices. Unicast replies.
@ SKIP
Kill switch: sends no 0x2C and applies no post-step pause.
@ SEND_WITH_ACK
Like SEND, but also sets CTRL1_ACK for an always-alive target (0x10) — the shape every corpus hub (VE...
const char * device_capability_class_name(DeviceType type)
Get a human‑readable name for a capability class.
std::string build_device_yaml_snippet(DeviceType type, uint8_t subtype, const std::string &device_id, bool metadata_complete, bool inverted, bool low_power)
Build the ready-to-paste YAML block describing a device, for both a fully-decoded device and one whos...
constexpr uint32_t PAIRING_RECENT_ONE_WAY_SIGHTING_WINDOW_MS
How recent a RecentOneWayPairingSighting has to be, relative to discover_and_pair() starting,...
@ ACCEPTED
The handler returned ReplyDisposition::ACCEPT for some received frame.
bool create_discovery_request(IoFrame &f, const uint8_t *own, uint8_t command, const uint8_t *dst, bool low_power, bool ack_capable, bool payload_enabled, uint8_t payload, const uint8_t *system_key)
Build a configurable discovery request command (0x28, 0x2A, or 0x2E).
uint8_t discovery_att_class(uint8_t flags)
Extract the ATT class field from a discovery response's Multi Information Byte.
constexpr uint32_t PAIRING_KEY_CHALLENGE_TIMEOUT_MS
Wait window for the device's 0x3C challenge.
const char * yaml_device_type_name(DeviceType type)
Return the YAML-friendly device-type name for types exposed in the Python schema.
std::string format_device_type_diagnostic(DeviceType type)
Human-readable device type string for diagnostics, including the raw numeric value.
const uint8_t * resolve_discovery_destination(uint8_t command, bool destination_auto, const uint8_t destination[NODE_ID_SIZE])
Resolve the destination address for a discovery command.
DiscoveryResponseInfo decode_discovery_response(const IoFrame &frame, IoDevice &device, std::string &device_id)
Decode a discovery-response payload (CMD_DISCOVER_RESP 0x29 or CMD_DISCOVER_SPE_RESP 0x2B — both carr...
static const char *const TAG
bool create_key_transfer(IoFrame &f, IoFrame &old_frame, const uint8_t *dst, const uint8_t *src, const uint8_t key[AES_KEY_SIZE], const uint8_t challenge[HMAC_SIZE])
Build a key-transfer frame (0x32) containing the system key encrypted with the transfer key.
Device discovery and key-exchange engine for IO-Homecontrol pairing.
Command builders for the IO‑Homecontrol protocol.
Extended discovery-response fields (manufacturer, Multi Information Byte, backbone address,...
bool has_extended
data_len >= DISCOVERY_RESP_FULL_SIZE (mfr/flags/timestamp present).
uint8_t backbone[NODE_ID_SIZE]
Backbone address as reported by the device.
uint8_t manufacturer
Raw manufacturer ID; name via manufacturer_name().
uint8_t flags
Multi Information Byte; decode with DISCOVERY_FLAGS_* masks.
Runtime state of a paired IO‑Homecontrol device.
bool inverted
True if open/close positions are swapped (e.g., horizontal awning).
uint8_t subtype
Device subtype (manufacturer‑specific).
uint32_t last_seen_ms
millis() of the last frame received from this device (any command), 0 = never.
uint8_t node_id[NODE_ID_SIZE]
Device's 3‑byte radio address.
DeviceType type
Device type (shutter, awning, etc.).
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
uint8_t data[FRAME_MAX_DATA_SIZE]
Command parameters (0–23 bytes). Never includes mac.
uint8_t src[NODE_ID_SIZE]
Source node ID (3 bytes).
uint8_t dst[NODE_ID_SIZE]
Destination node ID (3 bytes).
uint8_t data_len
Actual length of data.
How one listen window is to be spent — everything ExchangeEngine::listen() needs; everything else is ...
uint32_t request_freq
Channel the request went out on (Hz).
uint32_t window_ms
Total time budget for this listen, in milliseconds.
uint32_t linger_dwell_ms
Length of the preamble/sync extension, in milliseconds.
ListenPolicy policy
Which channels this listen covers.
bool linger_on_preamble
Extend the listen instead of hopping while the chip reports a preamble or sync word,...
std::function< void()> on_hop
Called on every hop, for callers that count hops in their own telemetry (pairing does; the exchange l...
Raw packet received from the radio.
uint32_t freq_hz
Frequency the packet was received on (Hz).
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.
uint16_t pairing_discovery_preamble
Preamble for the discovery broadcast (0x28/0x2E) start frame.
One piece of advice, with the node/RSSI it pertains to (if any).
Context object that lives for the duration of a single pairing attempt.
IoFrame req
Outbound frame buffer (reused across all phases).
bool discovery_low_power
True when discovery reported POWER_SAVE_LOW_POWER.
PairingState state
Current state machine state.
IoFrame resp
Inbound frame buffer (holds key‑confirm response).
RadioRxPacket packet
Raw radio capture for the current phase.
IoDevice device
Resolved device metadata after discovery (node_id, type, subtype, etc.).
IoFrame key_init
Key‑init frame retained for key‑transfer IV derivation.
std::string device_id
Hex string representation of the paired node ID.
bool discovery_metadata_complete
True when discovery carried type/subtype bytes.
IoFrame rx
Raw RX frame during waiting phases (discovery, challenge, confirm).
Runtime tuning configuration for pairing and radio diagnostics.