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 "config_failed";
76 this->event_count_ = 0;
77 this->heard_count_ = 0;
78 this->truncated_ =
false;
79 this->start_ms_ = millis();
80 this->end_ms_ = this->start_ms_;
84 this->discovery_attempts_ = 0;
85 this->lbt_retries_ = 0;
86 this->has_paired_device_ =
false;
87 memset(this->paired_node_id_, 0,
sizeof(this->paired_node_id_));
89 this->advice_codes_.clear();
93 int16_t rssi, uint8_t aux,
bool oneway) {
95 if (this->heard_count_ < UINT16_MAX)
99 this->truncated_ =
true;
102 PairingTelemetryEvent &
event = this->events_[this->event_count_++];
106 if (src !=
nullptr) {
111 if (dst !=
nullptr) {
118 event.oneway = oneway;
136 if (this->lbt_retries_ < COUNTER_SATURATION_MAX)
137 this->lbt_retries_++;
144 this->phase_ =
phase;
150 this->end_ms_ = millis();
155 this->has_paired_device_ =
true;
157 this->paired_device_type_ = type;
161 if (this->discovery_attempts_ < COUNTER_SATURATION_MAX)
162 this->discovery_attempts_++;
169 TAG,
"Pairing attempt summary: outcome=%s phase=%s attempts=%u lbt=%u dur_ms=%" PRIu32
" heard=%u events=%u%s",
170 outcome_name(this->outcome_),
pairing_stage_name(this->phase_), this->discovery_attempts_, this->lbt_retries_,
171 this->
duration_ms(), this->heard_count_, this->event_count_, this->truncated_ ?
" (truncated)" :
"");
172 for (uint8_t i = 0; i < this->event_count_; i++) {
175 ESP_LOGI(
TAG,
" [%4" PRIu32
" ms] %s cmd=0x%02X src=%s rssi=%d", event.millis_offset,
176 event_kind_name(event.kind), event.cmd,
node_id_to_string(event.src_node).c_str(), event.rssi);
178 ESP_LOGI(
TAG,
" [%4" PRIu32
" ms] %s cmd=0x%02X", event.millis_offset, event_kind_name(event.kind), event.cmd);
180 ESP_LOGI(
TAG,
" [%4" PRIu32
" ms] %s -> %s", event.millis_offset, event_kind_name(event.kind),
183 ESP_LOGI(
TAG,
" [%4" PRIu32
" ms] %s aux=%u rssi=%d", event.millis_offset, event_kind_name(event.kind),
184 event.aux, event.rssi);
187 if (this->has_paired_device_) {
194 char buf[RESULT_SENSOR_STRING_BUFFER_SIZE];
195 const std::string node = this->has_paired_device_ ?
node_id_to_string(this->paired_node_id_) :
"-";
196 const std::string type = this->has_paired_device_ ?
device_type_name(this->paired_device_type_) :
"-";
197 const std::string advice = this->advice_codes_.empty() ?
"none" : this->advice_codes_;
198 snprintf(buf,
sizeof(buf),
199 "v1; outcome=%s; phase=%s; node=%s; type=%s; attempts=%u; lbt=%u; dur_ms=%" PRIu32
"; heard=%u; advice=%s",
200 outcome_name(this->outcome_),
pairing_stage_name(this->phase_), node.c_str(), type.c_str(),
201 this->discovery_attempts_, this->lbt_retries_, this->duration_ms(), this->heard_count_, advice.c_str());
202 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_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.
PairingState
State machine for the three‑phase pairing flow.
@ IDLE
No pairing in progress; idle state.
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
DeviceType
Device type identifiers reported by IO‑Homecontrol products.
@ UNKNOWN
Unknown/unspecified device.
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....
@ HOP
The radio hopped to a different channel while waiting.
@ 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.
static constexpr const char * TAG
static constexpr uint8_t CTRL0_PROTOCOL_1W
Bit 5: 1=OneWay protocol, 0=TwoWay protocol.
const char * device_type_name(DeviceType type)
Convert a DeviceType to a lowercase string identifier.
static constexpr uint8_t PAIRING_TELEMETRY_MAX_EVENTS
Maximum number of events recorded per pairing attempt.
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
All three phases completed successfully.
@ CONFIG_FAILED
Key exchange succeeded but SetConfig1 failed (still counted as paired).
@ NO_RESPONSE
No device responded to discovery.
@ NONE
No attempt has completed yet (initial state).
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.
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().