7#include "esphome/core/log.h"
31const char *
const TAG =
"home_io_control";
32constexpr size_t DEVICE_TYPE_HEX_STRING_BUFFER_SIZE = 8;
42 return "wait_discover_response";
46 return "wait_key_challenge";
48 return "tx_key_transfer";
50 return "wait_key_confirm";
52 return "register_device";
76 ESP_LOGW(
TAG,
"No device responded to discovery");
79 ESP_LOGW(
TAG,
"No valid discovery response received");
88const char *yaml_device_type_name(
DeviceType type) {
91 return "venetian_blind";
93 return "roller_shutter";
97 return "window_opener";
99 return "garage_opener";
103 return "gate_opener";
105 return "rolling_door_opener";
113 return "heating_temperature_interface";
115 return "on_off_switch";
117 return "horizontal_awning";
119 return "curtain_track";
121 return "intrusion_alarm";
128std::string format_device_type_hex(
DeviceType type) {
129 char buf[DEVICE_TYPE_HEX_STRING_BUFFER_SIZE];
130 snprintf(buf,
sizeof(buf),
"0x%02X",
static_cast<uint8_t
>(type));
131 return std::string(buf);
135std::string format_device_type_diagnostic(
DeviceType type) {
137 std::string raw = format_device_type_hex(type);
138 if (name !=
nullptr && strcmp(name,
"unknown") != 0) {
139 return std::string(name) +
" (" + raw +
")";
146std::string format_device_type_for_yaml(
DeviceType type) {
147 const char *name = yaml_device_type_name(type);
148 if (name !=
nullptr) {
149 return std::string(
"\"") + name +
"\"";
151 return format_device_type_hex(type);
156 switch (capability_class) {
184 bool saw_traffic =
false;
185 const uint32_t deadline = millis() + timeout_ms;
186 while ((int32_t) (deadline - millis()) > 0) {
187 const uint32_t remaining_ms = deadline - millis();
189 if (!this->
radio_->wait_for_packet(packet, slice))
212 bool saw_traffic =
false;
213 const uint32_t deadline = millis() + timeout_ms;
214 while ((int32_t) (deadline - millis()) > 0) {
215 const uint32_t remaining_ms = deadline - millis();
217 if (!this->
radio_->wait_for_packet(packet, slice))
227 ESP_LOGW(
TAG, saw_traffic ?
"Key exchange: no valid challenge received" :
"Key exchange: no challenge received");
237 std::string &device_id) {
315 ESP_LOGW(
TAG,
"Key exchange failed");
349 ESP_LOGI(
TAG,
"Starting device discovery...");
357 log_discovery_diagnostic(disc_disp);
378 const char *platform = pairing_platform_name(capability_class);
379 const std::string type_diag = format_device_type_diagnostic(context.
device.
type);
380 const std::string type_yaml = format_device_type_for_yaml(context.
device.
type);
381 std::string extra_lines;
383 if (platform ==
nullptr) {
386 "Device %s paired successfully, but this repo does not yet expose an ESPHome platform for type=%s "
387 "class=%s subtype=%u.",
391 "No ready-to-paste YAML was generated. If you want to experiment manually, choose the most likely "
392 "platform and set io_device_type: %s.",
394 ESP_LOGW(
TAG,
"Please file a GitHub issue with this device type, subtype, model, and the pairing log so support "
397 ESP_LOGW(
TAG,
"Device %s paired successfully, but the discovery response did not include type/subtype metadata.",
399 ESP_LOGW(
TAG,
"No ready-to-paste YAML was generated. Add the device ID to the correct cover/light/switch entry "
400 "manually and leave io_device_type/io_subtype unset for now.");
401 ESP_LOGW(
TAG,
"Please file a GitHub issue with the pairing log and device model so this discovery edge case can "
412 extra_lines +=
" invert_position: true\n";
414 std::string subtype_line;
416 subtype_line =
" io_subtype: " + std::to_string(context.
device.
subtype) +
"\n";
420 "Device %s paired successfully! Add this to your YAML:\n"
422 " - platform: home_io_control\n"
424 " home_io_control_id: home_io_hub\n"
425 " io_device_id: \"%s\"\n"
426 " io_device_type: %s\n"
429 context.
device_id.c_str(), platform, context.
device_id.c_str(), type_yaml.c_str(), subtype_line.c_str(),
430 extra_lines.c_str());
433 ESP_LOGW(
TAG,
"This device did not report a subtype during discovery, so io_subtype was omitted. The controller "
434 "will try to learn it later at runtime.");
435 }
else if (yaml_device_type_name(context.
device.
type) ==
nullptr) {
437 "This snippet uses the raw device type %s because the project does not yet expose a named YAML alias "
439 type_yaml.c_str(), type_diag.c_str());
440 ESP_LOGW(
TAG,
"Please file a GitHub issue with this type, subtype, device model, and the pairing log so support "
std::map< std::string, IoDevice > devices_
bool send_and_receive_(const IoFrame &request, IoFrame &response, uint32_t freq)
Main request/response exchange with retry and automatic authentication.
decisions::PairingDiscoveryDisposition run_discovery_phase_(pairing::PairingContext &context)
Phase 1: broadcast discovery (0x28) and wait for a device response (0x29).
void record_exchange_debug_(const char *stage, uint8_t tries, bool saw_challenge)
bool finalize_pairing_configuration_(pairing::PairingContext &context)
Phase 3: send SetConfig1 (0x6F) to finalize device configuration.
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) from target device during pairing key exchange.
bool run_key_exchange_phase_(pairing::PairingContext &context)
Phase 2: authenticated key exchange (0x31 → 0x3C → 0x32 → 0x33).
virtual bool discover_and_pair()
Discover and pair a device that is in pairing mode.
decisions::PairingDiscoveryDisposition wait_for_discovery_response_(uint32_t timeout_ms, RadioRxPacket &packet, IoFrame &response_frame)
Wait for a discovery response (0x29) during pairing.
uint8_t node_id_[NODE_ID_SIZE]
static void parse_device_from_discovery(const IoFrame &frame, IoDevice &device, std::string &device_id)
Parse a discovery response frame into device metadata and ID.
IO-Homecontrol ESPHome component — protocol controller.
Pure transition helpers for hub-owned exchange and pairing frame decisions.
Internal helpers shared by the hub implementation .cpp files.
Internal pairing-state model for hub‑owned discovery and key‑exchange flows.
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.
PairingDiscoveryDisposition classify_pairing_discovery_response(const IoFrame &candidate)
Decide if a frame is a valid discovery response (0x29) during pairing.
uint32_t response_wait_slice_ms(uint32_t remaining_ms)
Slice remaining wait time into bounded intervals to allow frequency hopping.
constexpr uint32_t PAIRING_DISCOVERY_RESPONSE_TIMEOUT_MS
Discovery wait window after sending 0x28.
constexpr uint32_t PAIRING_KEY_CHALLENGE_TIMEOUT_MS
Wait window for the device's 0x3C challenge.
PairingState
State machine for the three‑phase pairing flow.
@ 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.
@ 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.
@ IDLE
No pairing in progress; idle state.
@ FAILED
Pairing failed (timeout, radio error, or protocol violation).
static constexpr uint8_t DEVICE_METADATA_SIZE
Packed device metadata uses two bytes where the high 8 bits carry the upper type bits and the low byt...
static constexpr float UNKNOWN_POSITION
Sentinel value meaning "position is not known yet".
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.
@ GATE_OPENER
Gate opener.
@ HEATING_TEMPERATURE_INTERFACE
Heating temperature interface.
@ UNKNOWN
Unknown/unspecified device.
@ INTRUSION_ALARM
Intrusion alarm.
@ WINDOW_OPENER
Window opening actuator.
@ HORIZONTAL_AWNING
Horizontal awning (open/close inverted).
@ ROLLING_DOOR_OPENER
Rolling door opener.
@ ON_OFF_SWITCH
Generic on/off switch.
@ SCREEN
Insect/privacy screen.
@ VENETIAN_BLIND
Venetian blind.
@ ROLLER_SHUTTER
Roller shutter.
@ CURTAIN_TRACK
Curtain track.
@ GARAGE_OPENER
Garage door opener.
DeviceCapabilityClass device_capability_class(DeviceType type)
Map a raw IO‑Homecontrol type to the closest ESPHome/Home Assistant entity family.
bool default_inverted_for_type(DeviceType type)
Determine whether a device type has inverted position mapping by default.
const char * device_type_name(DeviceType type)
Convert a DeviceType to a lowercase string identifier.
static constexpr uint8_t CMD_KEY_CONFIRM
Device confirms key was received.
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.
DeviceType decode_packed_device_type(uint8_t type_msb, uint8_t type_subtype)
Decode a protocol-packed device type from two metadata bytes.
bool parse(const uint8_t *buf, uint8_t buf_len, IoFrame &f)
Parse a wire buffer into a parsed IoFrame (validates length and CTRL0).
const char * device_capability_class_name(DeviceType type)
Get a human‑readable name for a capability class.
static constexpr uint32_t FREQ_CH2
Channel 2: 868.95 MHz (1W and 2W, TX channel).
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.
bool create_discover(IoFrame &f, const uint8_t *own)
Build a discovery broadcast (0x28).
uint8_t decode_packed_device_subtype(uint8_t type_subtype)
Decode a protocol-packed device subtype from the second metadata byte.
DeviceCapabilityClass
High‑level capability class derived from DeviceType.
@ SWITCH
Binary on/off switch.
@ COVER
Position‑controlled cover (shutter/blind/awning).
@ LIGHT
Binary on/off light.
static constexpr uint16_t LONG_PREAMBLE
Preamble is a sequence of 0xAA bytes that precedes every frame.
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.
Command builders for the IO‑Homecontrol protocol.
Runtime state of a paired IO‑Homecontrol device.
float target
Target position the device is moving toward.
bool inverted
True if open/close positions are swapped (e.g., horizontal awning).
float position
Current position: 0=open, 100=closed, or UNKNOWN_POSITION.
uint8_t subtype
Device subtype (manufacturer‑specific).
uint8_t node_id[NODE_ID_SIZE]
Device's 3‑byte radio address.
DeviceType type
Device type (shutter, awning, etc.).
bool is_stopped
True if device is not moving.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
uint8_t data[FRAME_MAX_DATA_SIZE]
Command parameters (0–23 bytes).
uint8_t src[NODE_ID_SIZE]
Source node ID (3 bytes).
uint8_t data_len
Actual length of data.
Raw packet received from the radio.
uint8_t len
Length of packet in bytes.
uint8_t data[RADIO_PACKET_BUFFER_SIZE]
Raw packet data buffer.
Context object that lives for the duration of a single pairing attempt.
IoFrame req
Outbound frame buffer (reused across all phases).
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).