12#include <esp_random.h>
65constexpr uint32_t KEY_EXTRACTION_AUTO_OFF_MS = 10 * 60 * 1000;
71constexpr uint8_t KEY_EXTRACTION_ADVERTISED_SUBTYPE = 0;
72constexpr uint8_t KEY_EXTRACTION_ID_GEN_MAX_ATTEMPTS = 16;
73constexpr const char *KEY_EXTRACTION_TIMEOUT_NAME =
"key_extraction_auto_off";
74constexpr uint32_t RANDOM_LOW_BYTE_MASK = 0xFF;
76constexpr uint32_t KEY_EXTRACTION_MID_ATTEMPT_TIMEOUT_MS = 5000;
99constexpr uint32_t KEY_EXTRACTION_POST_EXTRACT_GRACE_MS = 60000;
110constexpr const char *KEY_EXTRACTION_GRACE_TIMER_NAME =
"key_extraction_post_extract_grace";
121 transmit_(std::move(transmit)),
122 schedule_auto_off_(std::move(schedule_auto_off)) {}
125 for (uint8_t attempt = 0; attempt < KEY_EXTRACTION_ID_GEN_MAX_ATTEMPTS; attempt++) {
127 out[i] =
static_cast<uint8_t
>(esp_random() & RANDOM_LOW_BYTE_MASK);
150 if (this->armed_callback_)
151 this->armed_callback_(
false);
162 "Key extraction: ARMED for 10 minutes, throwaway ID %s. Put your existing hub into pairing/add-device "
171 this->schedule_auto_off_(KEY_EXTRACTION_TIMEOUT_NAME, KEY_EXTRACTION_AUTO_OFF_MS, [
this]() {
178 ESP_LOGW(
detail::TAG,
"Key extraction: window expired, no pairing attempt seen. Disarming.");
180 ESP_LOGW(
detail::TAG,
"Key extraction: window expired while in progress (reached stage=%s). Disarming.",
186 if (this->armed_callback_)
187 this->armed_callback_(
true);
195 this->handle_discover_(frame);
198 if (memcmp(frame.
dst, this->key_extraction_ctx_.throwaway_id,
NODE_ID_SIZE) != 0)
201 this->handle_discover_confirm_(frame);
205 this->handle_key_init_(frame);
209 this->handle_key_transfer_(frame);
213 this->handle_address_req_(frame);
217 this->handle_address_challenge_(frame);
223void KeyExtractionResponder::broadcast_reply_(
const IoFrame &frame) {
254 const uint16_t preamble =
256 this->transmit_(frame,
FREQ_CH1, preamble);
257 this->transmit_(frame,
FREQ_CH3, preamble);
258 this->transmit_(frame,
FREQ_CH2, preamble);
261void KeyExtractionResponder::handle_discover_(
const IoFrame &frame) {
267 this->key_extraction_ctx_.advertised_type, this->key_extraction_ctx_.advertised_subtype,
268 KEY_EXTRACTION_MANUFACTURER_ID)) {
269 ESP_LOGW(
detail::TAG,
"Key extraction: failed to build discovery response");
278 this->broadcast_reply_(resp);
280 ESP_LOGI(
detail::TAG,
"Key extraction: replied to discovery from hub %s with throwaway ID %s",
284void KeyExtractionResponder::handle_discover_confirm_(
const IoFrame &frame) {
290 ESP_LOGW(
detail::TAG,
"Key extraction: failed to build discovery-confirm ack");
295 this->broadcast_reply_(resp);
297 ESP_LOGI(
detail::TAG,
"Key extraction: acknowledged discovery confirm from hub %s",
301void KeyExtractionResponder::handle_key_init_(
const IoFrame &frame) {
309 this->key_extraction_ctx_.challenge)) {
310 ESP_LOGW(
detail::TAG,
"Key extraction: failed to build challenge request");
315 this->broadcast_reply_(resp);
320void KeyExtractionResponder::handle_key_transfer_(
const IoFrame &frame) {
322 ESP_LOGW(
detail::TAG,
"Key extraction: key-transfer payload too short (%u bytes)", frame.data_len);
330 this->broadcast_reply_(resp);
332 ESP_LOGW(
detail::TAG,
"Key extraction: failed to build key confirm");
339 "Key extraction: still listening for up to %" PRIu32
340 " more seconds in case the hub verifies this device's address (CMD_ADDRESS_REQ/0x36) — leave the "
341 "switch on until it turns off on its own.",
342 KEY_EXTRACTION_POST_EXTRACT_GRACE_MS / 1000);
355void KeyExtractionResponder::handle_address_req_(
const IoFrame &frame) {
362 if (memcmp(frame.src, this->key_extraction_ctx_.hub_node_id,
NODE_ID_SIZE) != 0)
368 ESP_LOGW(
detail::TAG,
"Key extraction: failed to build address response");
371 this->broadcast_reply_(resp);
376void KeyExtractionResponder::handle_address_challenge_(
const IoFrame &frame) {
379 if (memcmp(frame.src, this->key_extraction_ctx_.hub_node_id,
NODE_ID_SIZE) != 0)
385 ESP_LOGW(
detail::TAG,
"Key extraction: address challenge payload too short (%u bytes)", frame.data_len);
394 IoFrame our_address_resp;
400 frame.data, our_address_resp, this->key_extraction_ctx_.recovered_key)) {
401 ESP_LOGW(
detail::TAG,
"Key extraction: failed to build address challenge response");
404 this->broadcast_reply_(resp);
428 this->schedule_auto_off_(KEY_EXTRACTION_GRACE_TIMER_NAME, KEY_EXTRACTION_POST_EXTRACT_GRACE_MS, [
this]() {
444 ESP_LOGI(
detail::TAG,
"Key extraction: post-extraction grace window elapsed (stage=%s). Disarming.",
460void KeyExtractionResponder::log_result_() {
470 ESP_LOGW(
detail::TAG,
"========================================");
473 this->key_extraction_ctx_.recovered_key));
474 ESP_LOGW(
detail::TAG,
"========================================");
Owns the per-hub device table, update callbacks, and linked-remote associations.
Abstract radio driver for IO-Homecontrol.
Per-hub device table, update-callback fan-out, and linked-remote map.
Internal helpers shared by the hub implementation .cpp files.
void generate_challenge(uint8_t out[HMAC_SIZE])
Generate 6 random bytes for a challenge using the ESP32 hardware RNG.
constexpr const char * TAG
Shared log tag for hub-level messages.
void log_multiline_result(const char *tag, bool is_warning, const std::string &prefix, const std::string &message)
Log prefix followed by message, one line per log call rather than one call for the whole (possibly mu...
std::string build_key_extraction_report(const uint8_t node_id[NODE_ID_SIZE], const uint8_t key[AES_KEY_SIZE])
Build the ready-to-paste 2W system-key-extraction report: node_id:/system_key: as a home_io_control: ...
bool on_key_transfer(ResponderContext &ctx, const uint8_t transfer_payload[AES_KEY_SIZE])
Decide how to react to an inbound CMD_KEY_TRANSFER (0x32) while armed.
const char * responder_stage_name(ResponderState state)
Get a short, log/telemetry-friendly name for a responder state.
bool on_discover_confirm(ResponderContext &ctx)
Decide how to react to an inbound CMD_DISCOVER_CONFIRM (0x2C) addressed to our throwaway ID.
bool on_address_challenge(const ResponderContext &ctx)
Decide how to react to an inbound CMD_CHALLENGE_REQ (0x3C) — issued by the hub this time,...
bool on_key_init(ResponderContext &ctx, const uint8_t challenge[HMAC_SIZE], const uint8_t hub_node_id[NODE_ID_SIZE])
Decide how to react to an inbound CMD_KEY_INIT (0x31) addressed to our throwaway ID.
bool on_address_req(ResponderContext &ctx)
Decide how to react to an inbound CMD_ADDRESS_REQ (0x36) addressed to our throwaway ID.
@ ARMED_IDLE
Armed, listening for a discovery request (0x28).
@ DISARMED
Not armed; 0x28/0x2C/0x31/0x32 traffic is ignored.
@ SENT_ADDRESS_RESP
Answered a hub's CMD_ADDRESS_REQ (0x36) with our CMD_ADDRESS_RESP (0x37); waiting for the hub's own C...
@ EXTRACTED
System key recovered from a valid 0x32.
bool on_discover_request(ResponderContext &ctx, const uint8_t hub_node_id[NODE_ID_SIZE])
Decide how to react to an inbound CMD_DISCOVER_REQ (0x28) while armed.
static constexpr uint8_t CMD_DISCOVER_REQ
Broadcast discovery request.
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
static constexpr uint8_t CMD_KEY_TRANSFER
Send encrypted system key to device.
DeviceType
Device type identifiers reported by IO‑Homecontrol products.
@ ROLLER_SHUTTER
Roller shutter.
bool create_discover_resp(IoFrame &f, const uint8_t *own, const uint8_t *dst, DeviceType type, uint8_t subtype, uint8_t manufacturer_id)
Build a discovery response (0x29) — device side, used only by the key-extraction responder.
bool is_start(const IoFrame &f)
Check START flag.
static constexpr uint32_t FREQ_CH1
The protocol uses 3 frequency channels in the 868 MHz ISM band.
static constexpr uint32_t FREQ_CH3
Channel 3: 869.85 MHz (2W only).
static constexpr uint8_t HMAC_SIZE
Authentication HMAC is 6 bytes (truncated AES output).
bool create_address_resp_device_role(IoFrame &f, const uint8_t *own, const uint8_t *dst)
Build an address response (0x37) — device side, used only by the key-extraction responder.
bool create_key_confirm(IoFrame &f, const uint8_t *own, const uint8_t *dst)
Build a key-confirm frame (0x33) — device side, used only by the key-extraction responder.
static constexpr uint8_t CMD_KEY_INIT
Initiate key transfer to device.
std::function< bool(const IoFrame &frame, uint32_t freq_hz, uint16_t preamble)> TransmitFrameFn
Puts a frame on air on a given channel via the hub's protected transmit_frame_().
bool stored_node_id_is_valid(const uint8_t id[NODE_ID_SIZE])
Check if a stored node ID is valid (not all-zero, not all-0xFF).
static constexpr uint32_t FREQ_CH2
Channel 2: 868.95 MHz (1W and 2W, TX channel).
static constexpr uint8_t CMD_CHALLENGE_REQ
6-byte random challenge.
bool create_discover_confirm_ack(IoFrame &f, const uint8_t *own, const uint8_t *dst)
Build a discovery-confirm acknowledgement (0x2D) — device side, used only by the key-extraction respo...
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_challenge_req_device_role(IoFrame &f, const uint8_t *dst, const uint8_t *src, const uint8_t challenge[HMAC_SIZE])
Build a device-role challenge request (0x3C) — device side, used only by the key-extraction responder...
static constexpr uint8_t CMD_DISCOVER_CONFIRM
Confirm discovery to device.
std::function< void(const char *name, uint32_t delay_ms, std::function< void()> callback)> NamedTimeoutFn
Schedules a named, replace-on-same-name timeout on the hub's ESPHome scheduler.
static constexpr uint8_t BROADCAST_DISCOVER[NODE_ID_SIZE]
Broadcast address for device discovery (0x00003B).
static constexpr uint8_t BROADCAST_DISCOVER_ALT[NODE_ID_SIZE]
Alternate discovery / 1W broadcast address (0x00003F).
static constexpr uint8_t AES_KEY_SIZE
AES-128 key size.
bool create_challenge_resp_device_role(IoFrame &f, const uint8_t *dst, const uint8_t *src, const uint8_t challenge[HMAC_SIZE], const IoFrame &origin, const uint8_t *key)
Build a device-role challenge response (0x3D) — device side, used only by the key-extraction responde...
static constexpr uint8_t MANUFACTURER_SOMFY
Somfy (shutters, awnings, blinds).
static constexpr uint8_t CMD_ADDRESS_REQ
"Report your address" request.
Pure decision logic for the device-role "Accept Foreign Pairing" (system-key extraction) responder.
Command builders for the IO‑Homecontrol protocol.
Cryptographic helpers for the IO‑Homecontrol protocol.
Radio abstraction layer for IO-Homecontrol.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
uint8_t dst[NODE_ID_SIZE]
Destination node ID (3 bytes).
All runtime tunable parameters for pairing and radio diagnostics.
uint16_t cold_broadcast_reply_preamble
Preamble for a start-flagged key-extraction broadcast reply (0x29).
Context for one key-extraction arm cycle.
uint8_t throwaway_id[NODE_ID_SIZE]
Random per-arm-cycle node ID we advertise as ourselves.
uint8_t hub_node_id[NODE_ID_SIZE]
Foreign hub's real node ID, captured from the 0x31's src.
Runtime tuning configuration for pairing and radio diagnostics.