7#include "esphome/core/hal.h"
8#include "esphome/core/log.h"
20const char *
const TAG =
"home_io_control.pairing";
28constexpr size_t RESULT_SENSOR_STRING_BUFFER_SIZE = 256;
32constexpr uint8_t COUNTER_SATURATION_MAX = 0xFF;
41 return "invalid_response";
43 return "key_exchange_failed";
45 return "key_transfer_refused";
47 return "key_received";
49 return "hub_key_returned";
78 this->event_count_ = 0;
79 this->heard_count_ = 0;
81 this->truncated_ =
false;
82 this->start_ms_ = millis();
83 this->end_ms_ = this->start_ms_;
87 this->discovery_attempts_ = 0;
88 this->lbt_retries_ = 0;
89 this->has_paired_device_ =
false;
90 memset(this->paired_node_id_, 0,
sizeof(this->paired_node_id_));
92 this->has_key_refusal_ =
false;
93 this->key_refusal_code_ = 0;
94 this->key_pull_attempted_ =
false;
95 this->key_pull_unproven_ =
false;
96 this->has_discovered_device_ =
false;
97 memset(this->discovered_node_id_, 0,
sizeof(this->discovered_node_id_));
99 this->advice_codes_.clear();
103 int16_t rssi, uint8_t aux,
bool oneway) {
105 if (this->heard_count_ < UINT16_MAX)
106 this->heard_count_++;
108 if (this->event_count_ >= PAIRING_TELEMETRY_MAX_EVENTS) {
109 this->truncated_ =
true;
112 PairingTelemetryEvent &
event = this->events_[this->event_count_++];
116 if (src !=
nullptr) {
117 memcpy(event.src_node, src, NODE_ID_SIZE);
119 memset(event.src_node, 0, NODE_ID_SIZE);
121 if (dst !=
nullptr) {
122 memcpy(event.dst_node, dst, NODE_ID_SIZE);
124 memset(event.dst_node, 0, NODE_ID_SIZE);
128 event.oneway = oneway;
137 (frame.
ctrl0 & CTRL0_PROTOCOL_1W) != 0);
142 (frame.
ctrl0 & CTRL0_PROTOCOL_1W) != 0);
146 if (this->lbt_retries_ < COUNTER_SATURATION_MAX)
147 this->lbt_retries_++;
152 if (this->hop_count_ < UINT32_MAX)
162 this->phase_ =
phase;
168 this->end_ms_ = millis();
173 this->has_paired_device_ =
true;
174 memcpy(this->paired_node_id_, node_id, NODE_ID_SIZE);
175 this->paired_device_type_ = type;
179 this->has_discovered_device_ =
true;
180 memcpy(this->discovered_node_id_, node_id, NODE_ID_SIZE);
181 this->discovered_node_type_ = node_type;
185 if (this->discovery_attempts_ < COUNTER_SATURATION_MAX)
186 this->discovery_attempts_++;
193 "Pairing attempt summary: outcome=%s phase=%s attempts=%u lbt=%u dur_ms=%" PRIu32
" heard=%u hops=%" PRIu32
195 outcome_name(this->outcome_),
pairing_stage_name(this->phase_), this->discovery_attempts_,
196 this->lbt_retries_, this->
duration_ms(), this->heard_count_, this->hop_count_, this->event_count_,
197 this->truncated_ ?
" (truncated)" :
"");
198 for (uint8_t i = 0; i < this->event_count_; i++) {
204 ESP_LOGI(TAG,
" [pre-window] %s cmd=0x%02X src=%s rssi=%d", event_kind_name(event.kind), event.cmd,
207 ESP_LOGI(TAG,
" [%4" PRIu32
" ms] %s cmd=0x%02X src=%s rssi=%d", event.millis_offset,
208 event_kind_name(event.kind), event.cmd,
node_id_to_string(event.src_node).c_str(), event.rssi);
210 ESP_LOGI(TAG,
" [%4" PRIu32
" ms] %s cmd=0x%02X", event.millis_offset, event_kind_name(event.kind), event.cmd);
212 ESP_LOGI(TAG,
" [%4" PRIu32
" ms] %s -> %s", event.millis_offset, event_kind_name(event.kind),
215 ESP_LOGI(TAG,
" [%4" PRIu32
" ms] %s aux=%u rssi=%d", event.millis_offset, event_kind_name(event.kind),
216 event.aux, event.rssi);
219 if (this->has_paired_device_) {
220 ESP_LOGI(TAG,
" paired node=%s type=%s",
node_id_to_string(this->paired_node_id_).c_str(),
226 char buf[RESULT_SENSOR_STRING_BUFFER_SIZE];
227 const std::string node = this->has_paired_device_ ?
node_id_to_string(this->paired_node_id_) :
"-";
228 const std::string type = this->has_paired_device_ ?
device_type_name(this->paired_device_type_) :
"-";
229 const std::string advice = this->advice_codes_.empty() ?
"none" : this->advice_codes_;
230 snprintf(buf,
sizeof(buf),
231 "v1; outcome=%s; phase=%s; node=%s; type=%s; attempts=%u; lbt=%u; dur_ms=%" PRIu32
"; heard=%u; advice=%s",
232 outcome_name(this->outcome_),
pairing_stage_name(this->phase_), node.c_str(), type.c_str(),
233 this->discovery_attempts_, this->lbt_retries_, this->duration_ms(), this->heard_count_, advice.c_str());
234 return std::string(buf);
void record_rx_reject(const IoFrame &frame, int16_t rssi)
Record that we received a frame that parsed but was rejected by a classifier.
void increment_discovery_attempt()
Increment the discovery attempt counter (one call per discovery command retry).
void set_paired_device(const uint8_t node_id[NODE_ID_SIZE], DeviceType type)
Record the successfully paired device, if any.
PairingOutcome outcome() const
void set_phase(pairing::PairingState phase)
Record a pairing state-machine phase transition.
std::string result_sensor_string() const
Render the frozen v1; machine-readable result string.
uint32_t duration_ms() const
void record_recent_one_way_sighting(const RecentOneWayPairingSighting &sighting)
Seed telemetry with a 1W pairing-gesture frame observed shortly before this attempt began.
void record_tx(uint8_t cmd)
Record that we transmitted a frame.
void record_hop()
Record a frequency hop while waiting.
void log_summary() const
Emit a multi-line human-readable summary via ESP_LOGI. Call once, at the end of the attempt.
void record_lbt_defer(int16_t rssi)
Record a listen-before-talk defer (channel busy).
void begin()
Reset all state and start a new attempt. Call once at discover_and_pair() entry.
pairing::PairingState phase() const
void set_outcome(PairingOutcome outcome)
Record the final outcome of the attempt.
void record_rx(const IoFrame &frame, int16_t rssi)
Record that we received and accepted a frame.
void set_discovered_device(const uint8_t node_id[NODE_ID_SIZE], DeviceType node_type)
Record the device discovery accepted: its node ID and node type.
PairingState
State machine for the three‑phase pairing flow.
@ IDLE
No pairing in progress; idle state.
PairingTelemetryEventKind
Kind of a recorded telemetry event.
@ PHASE
The pairing state machine advanced to a new phase.
@ RX_REJECT
We received a frame that parsed but was rejected (wrong source, wrong command, etc....
@ TX
We transmitted a frame.
@ LBT_DEFER
A listen-before-talk check deferred a transmit because the channel was busy.
@ RX
We received and accepted a frame for the current wait (including a seeded pre-window sighting — see P...
const char * device_type_name(DeviceType type)
Convert a DeviceType to a lowercase string identifier.
DeviceType
IO-Homecontrol node types.
@ UNKNOWN
Unknown/unspecified device.
PairingOutcome
Final disposition of a pairing attempt, used by the result sensor string.
@ HUB_KEY_RETURNED
A pull returned the hub's own key from a node no platform drives (an Atlantic radiator's pairing node...
@ 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.
@ NONE
No attempt has completed yet (initial state).
@ KEY_TRANSFER_REFUSED
The device answered our key transfer with CMD_ERROR_RESP; the code is in PairingTelemetry::key_refusa...
@ KEY_RECEIVED
pairing_key_transfer: pull received a key that differs from ours and the device
const char * pairing_stage_name(pairing::PairingState state)
Get a short, log/telemetry-friendly name for a pairing state.
std::string node_id_to_string(const uint8_t id[NODE_ID_SIZE])
Format a 3‑byte node ID as a 6‑character uppercase hex string.
static const char *const TAG
Structured per-attempt telemetry recorder for the pairing flow.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
uint8_t ctrl0
Control byte 0: flags + length.
uint8_t src[NODE_ID_SIZE]
Source node ID (3 bytes).
uint8_t dst[NODE_ID_SIZE]
Destination node ID (3 bytes).
One recorded telemetry event.
uint32_t millis_offset
millis() at record time, relative to PairingTelemetry::begin().
A 1W pairing-gesture frame observed on the hub's normal passive RX path, remembered so a fresh discov...
uint8_t dst[NODE_ID_SIZE]
Destination node ID (the 1W broadcast address, in practice).
uint8_t src[NODE_ID_SIZE]
Sender node ID.
uint8_t cmd
Frame command byte.
int16_t rssi
RSSI in dBm at the time it was overheard.