|
Home IO Control
ESPHome add-on for IO-Homecontrol devices
|
Classes | |
| struct | ResponderContext |
| Context for one key-extraction arm cycle. More... | |
Enumerations | |
| enum class | ResponderState : uint8_t { DISARMED , ARMED_IDLE , SENT_DISCOVER_RESP , SENT_CONFIRM_ACK , SENT_CHALLENGE , EXTRACTED , SENT_ADDRESS_RESP } |
| State machine for the device-role key-extraction responder. More... | |
Functions | |
| const char * | responder_stage_name (ResponderState state) |
| Get a short, log/telemetry-friendly name for a responder state. | |
| 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. | |
| bool | on_discover_confirm (ResponderContext &ctx) |
| Decide how to react to an inbound CMD_DISCOVER_CONFIRM (0x2C) addressed to our throwaway ID. | |
| 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_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. | |
| bool | on_address_req (ResponderContext &ctx) |
| Decide how to react to an inbound CMD_ADDRESS_REQ (0x36) 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, challenging our own CMD_ADDRESS_RESP (0x37) rather than us challenging the hub. | |
|
strong |
State machine for the device-role key-extraction responder.
Definition at line 30 of file pairing_responder.h.
| bool esphome::home_io_control::pairing_responder::on_address_challenge | ( | const ResponderContext & | ctx | ) |
Decide how to react to an inbound CMD_CHALLENGE_REQ (0x3C) — issued by the hub this time, challenging our own CMD_ADDRESS_RESP (0x37) rather than us challenging the hub.
Valid only from SENT_ADDRESS_RESP: a hub-issued 0x3C only makes sense after we've told it our address. No new fields to store — the caller has everything it needs (ctx.recovered_key, ctx.throwaway_id, the inbound frame's own challenge bytes) to build 0x3D on the spot.
Unlike every other on_*() in this file, this function does NOT clear or advance state on success — it stays in SENT_ADDRESS_RESP precisely so a retried 0x3C is answered the same way a retried 0x31 is by on_key_init(). That makes it the only on_*() here that returns true without ever changing ctx; that is intentional, not an oversight.
| ctx | Responder context (read-only; not mutated). |
Definition at line 94 of file pairing_responder.cpp.
| bool esphome::home_io_control::pairing_responder::on_address_req | ( | ResponderContext & | ctx | ) |
Decide how to react to an inbound CMD_ADDRESS_REQ (0x36) addressed to our throwaway ID.
Valid from EXTRACTED (first reply) and SENT_ADDRESS_RESP (a hub retry — resend the same 0x37, nothing regenerated, matching on_discover_request()'s established retry idiom). No-op from any earlier state: a hub that hasn't completed the key exchange yet has no business asking for our address. No-op from DISARMED/ARMED_IDLE etc. too, same as every other on_*() here.
| ctx | Responder context (mutated: state only). |
Definition at line 87 of file pairing_responder.cpp.
| bool esphome::home_io_control::pairing_responder::on_discover_confirm | ( | ResponderContext & | ctx | ) |
Decide how to react to an inbound CMD_DISCOVER_CONFIRM (0x2C) addressed to our throwaway ID.
A hub sends 0x2C directly to a device it just discovered and, in general, will not proceed to the key exchange until that device answers with CMD_DISCOVER_CONFIRM_ACK (0x2D) — the step between discovery and key-init in a real pairing (tests/corpus/captures/pairing/velux_kux100_pairing_full.yaml, and this project's own responder answering a real hub's 0x2C in tests/corpus/captures/pairing/velux_kig300_pairing_key_extraction_success.yaml — the fixed, successful retest of velux_kig300_pairing_key_extraction_stall.yaml's stalled session, where that same hub's 0x2C previously went unanswered). Hub strictness varies: some retry 0x2C indefinitely without it, others eventually send 0x31 anyway, which is why on_key_init() also accepts a key-init straight from SENT_DISCOVER_RESP.
Valid from SENT_DISCOVER_RESP (first confirm) and SENT_CONFIRM_ACK (a hub retry after missing our 0x2D — resend the same bare ack, nothing is regenerated). No-op from any other state: like on_discover_request(), an early-phase frame must never pull an exchange already past this point back down.
| ctx | Responder context (mutated: state only). |
Definition at line 57 of file pairing_responder.cpp.
| bool esphome::home_io_control::pairing_responder::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.
Valid from ARMED_IDLE (first response — replies using the throwaway ID/advertised type already stored in ctx by the caller at arm time) and SENT_DISCOVER_RESP (a hub retry — resend the same values without regenerating anything). No-op from SENT_CONFIRM_ACK or SENT_CHALLENGE: a discovery request must never pull an in-flight exchange back a phase. That silence is what a real device does too — in tests/corpus/captures/pairing/velux_kux100_pairing_full.yaml the hub re-broadcasts 0x28 right after the confirm-ack (landing in SENT_CONFIRM_ACK) and the device pointedly does not answer, before the hub moves on to the key exchange. A second, independent hub does the same thing in tests/corpus/captures/pairing/velux_kig300_pairing_key_extraction_success.yaml (re-broadcast omitted from that capture's frame list as redundant, per its own notes). Neither of those mid-exchange states depends on hub_node_id — ARMED_IDLE/SENT_DISCOVER_RESP accept unconditionally too, since ctx.hub_node_id isn't meaningfully populated this early (it is first captured by on_key_init(), below) and the original design never restricted this case.
Also valid from EXTRACTED and SENT_ADDRESS_RESP — but, unlike every other case above, only when hub_node_id matches ctx.hub_node_id (the hub this responder actually extracted a key from, captured by on_key_init()). These two states are a completed attempt, so a fresh 0x28 from the same hub here cannot be the mid-exchange rebroadcast the corpus evidence above documents — every corpus capture with one shows it landing between the confirm-ack and the key transfer, never after extraction — and is therefore a genuinely new attempt by that hub trying again. Without accepting this case, one successful extraction would deafen the responder to every later attempt by the same hub for the rest of the 10-minute arm window, recoverable only by manually disarming and re-arming the switch.
The hub-identity check is what hub_node_id is for: CMD_DISCOVER_REQ is a broadcast handled before the throwaway-ID destination filter (KeyExtractionResponder::try_handle_frame(), key_extraction_responder.cpp), so without it any hub in radio range — not necessarily the one this responder is actually doing business with — could knock a live post-extraction address-verification round (0x36→0x37→0x3C→0x3D with the real hub) back to SENT_DISCOVER_RESP merely by broadcasting its own unrelated 0x28. Restricting the restart to the same hub closes that hole while still fixing the "second attempt permanently dead" bug above for its actual repro case (the same hub, or test rig, starting a fresh attempt without a switch toggle).
| ctx | Responder context (mutated: state only). |
| hub_node_id | The inbound frame's src (the broadcasting hub's real node ID) — only consulted from EXTRACTED/SENT_ADDRESS_RESP; ignored otherwise. |
Definition at line 35 of file pairing_responder.cpp.
| bool esphome::home_io_control::pairing_responder::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.
Valid from SENT_DISCOVER_RESP and SENT_CONFIRM_ACK (first key-init: stores challenge and hub_node_id, then advances — accepted from both because not every hub waits for our 0x2D, see on_discover_confirm()) and SENT_CHALLENGE (a hub retry after missing our 0x3C — challenge is discarded and the previously-stored one is reused instead, since the hub's eventual 0x32 must be decrypted with the exact challenge bytes it actually received; SX1262's slow TX→RX turnaround makes this retry path the expected common case, not an edge case). No-op from any other state.
| ctx | Responder context (mutated: state, and on the first call challenge/hub_node_id). |
| challenge | Caller-generated 6 random bytes (crypto::generate_challenge()); only consumed on the first 0x31 of an attempt. |
| hub_node_id | The inbound frame's src (the foreign hub's real node ID). |
challenge directly — they may differ on a retry). Definition at line 64 of file pairing_responder.cpp.
| bool esphome::home_io_control::pairing_responder::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.
Valid only from SENT_CHALLENGE. Recovers the system key from transfer_payload using ctx.challenge via recover_system_key_from_transfer() (proto_commands.h) and, on success, transitions to EXTRACTED. No-op (state unchanged) from any other state, or if decoding fails.
| ctx | Responder context (mutated: state, and on success recovered_key). |
| transfer_payload | 16-byte CMD_KEY_TRANSFER payload (frame.data). |
Definition at line 78 of file pairing_responder.cpp.
| const char * esphome::home_io_control::pairing_responder::responder_stage_name | ( | ResponderState | state | ) |
Get a short, log/telemetry-friendly name for a responder state.
| state | Responder state. |
Definition at line 15 of file pairing_responder.cpp.