Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
proto_commands.h
Go to the documentation of this file.
1#pragma once
2
3/// @file proto_commands.h
4/// @brief Command builders for the IO‑Homecontrol protocol.
5/// @ingroup hioc_protocol
6///
7/// This module provides builder functions that populate IoFrame structures for
8/// the various commands used in discovery, pairing, control, and status operations.
9/// All builders follow the same pattern: fill an IoFrame with CTRL0/CTRL1 flags,
10/// addresses, command ID, and optional payload.
11///
12/// Position encoding:
13/// - IO protocol position values: 0 = fully open, 100 = fully closed, for a non-inverted
14/// device — but this is per-device, not a universal wire constant: IoDevice::inverted
15/// devices (e.g. horizontal awnings) have it backwards (0 = fully closed, 100 = fully
16/// open). Never hardcode "0 = open" in caller code without checking inversion first.
17/// - Use create_execute_position() for numeric positions (0–100).
18/// - Use create_execute_command() for named commands: CoverCommand::STOP,
19/// CoverCommand::FAVORITE, CoverCommand::VENT.
20/// - Use create_force_open() for CoverCommand::FORCE_OPEN — it takes the target "fully open"
21/// position explicitly rather than assuming 0
22/// - The Home Assistant layer maps HA's 1.0=open/0.0=closed to the IO scale via
23/// ha_position = 1.0 - (io_position / 100.0), or the inverted form for IoDevice::inverted
24/// devices; see platform_cover.h.
25///
26/// Low‑power flag and preamble handling:
27/// - CTRL1_LOW_POWER is a per‑device property on the command path. Every device‑addressed
28/// builder on that path takes an explicit `low_power` argument, set from the target's
29/// YAML‑declared `low_power` class (default false: an always‑listening receiver).
30/// Battery/solar devices that duty‑cycle their receiver get the flag; mains devices do not,
31/// matching a reference hub's own traffic. The pairing and device‑role builders
32/// (`create_key_init`, `create_key_transfer`, `create_set_config1`,
33/// `create_status_update_resp`) keep a fixed value instead — see ADR 0029's out‑of‑scope note.
34/// `create_discover_confirm()` is the one pairing builder that does NOT keep a fixed value: its
35/// `low_power` follows the discovered device's own self-reported power-save class instead, the
36/// same per-device-property treatment as every non-pairing builder above — every real hub in
37/// this project's corpus mirrors that class exactly on its own 0x2C.
38/// - The preamble follows the flag. For a start frame the exchange engine uses LONG_PREAMBLE
39/// (1024 bytes) when CTRL1_LOW_POWER is set — the wake‑up burst for a sleeping receiver —
40/// and the runtime‑tunable `normal_start_preamble` otherwise; follow‑up frames use the
41/// driver's response_preamble() (exchange_engine.cpp). Both derive from the same
42/// per‑device property, so the flag never promises a wake-up burst the device does not need,
43/// with two exceptions. Pairing's directed start frames (0x2C, 0x31, 0x6F) never use a longer
44/// preamble than the discovery request the device just answered
45/// (PairingEngine::pairing_start_preamble_()). And send_and_receive() may lead a low-power
46/// target's tries with the short preamble while the target is believed awake (ADR 0040), with
47/// the frame bytes — flag included — unchanged.
48
49#include "proto_codecs.h"
50#include "proto_constants.h"
51#include "proto_device_model.h"
52#include "proto_frame.h"
53
54namespace esphome {
55namespace home_io_control {
56
57/// @brief Build a position execute command (0x00) to move a device to a numeric position.
58///
59/// Encodes a 0–100 position value into the standard 8-byte execute payload with
60/// originator, ACEI, and functional parameter fields. The wire encoding doubles
61/// the position value (0→0x00, 100→0xC8).
62/// @param f IoFrame to populate.
63/// @param own Controller's 3‑byte node ID (source address).
64/// @param dst Target device's 3‑byte node ID (destination address).
65/// @param low_power True if target is battery/solar‑powered (sets CTRL1_LOW_POWER).
66/// @param position Desired position 0–100 (0=fully open, 100=fully closed).
67/// @return true on success; false if position > 100.
68/// @param silent Send the reference hub's "silent operation" extended block, which makes the motor
69/// travel more slowly. Applies to position moves only — STOP has no speed, and no capture
70/// yet shows what the toggle does to FAVORITE/VENT or tilt, so those are left alone rather
71/// than guessed at.
72bool create_execute_position(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t position,
73 bool silent = false);
74
75/// @brief Build a named-command execute frame (0x00) for STOP, FAVORITE, or VENT.
76///
77/// Each maps to a specific wire encoding in the 6-byte special CMD_EXECUTE payload:
78/// - STOP: main=0xD2, modifier=0x00
79/// - FAVORITE: main=0xD8, modifier=0x00
80/// - VENT: main=0xD8, modifier=0x03
81///
82/// This cleanly separates "move to position X" from "execute named action"
83/// without overloading a single numeric parameter.
84///
85/// FORCE_OPEN is NOT handled here — see create_force_open() instead. Unlike these three, it
86/// needs a device-specific "fully open" wire position (0 or 100 depending on inversion), which
87/// this generic dispatch has no way to supply; passing CoverCommand::FORCE_OPEN returns false.
88/// @param f IoFrame to populate.
89/// @param own Controller's 3‑byte node ID (source address).
90/// @param dst Target device's 3‑byte node ID (destination address).
91/// @param low_power True if target is battery/solar‑powered (sets CTRL1_LOW_POWER).
92/// @param cmd Named command to execute (STOP, FAVORITE, or VENT).
93/// @return true on success; false for invalid/unsupported command.
94/// @param silent Use the silent (slower) travel profile. Honoured for FAVORITE only — STOP has no
95/// travel speed, and nothing has been captured for VENT.
96bool create_execute_command(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, CoverCommand cmd,
97 bool silent = false);
98
99/// @brief Build a force-open execute frame (0x00): move to the device's wire-scale "fully open"
100/// position at elevated ACEI priority (level 0, protection_human) instead of the usual
101/// user_high level — see EXECUTE_ACEI_FORCE_OPEN in proto_commands.cpp for why priority
102/// elevation, not a special position byte, is the protocol's real mechanism for getting past an
103/// environmental soft lock.
104///
105/// @param f IoFrame to populate.
106/// @param own Controller's 3‑byte node ID (source address).
107/// @param dst Target device's 3‑byte node ID (destination address).
108/// @param low_power True if target is battery/solar‑powered (sets CTRL1_LOW_POWER).
109/// @param open_position The device's wire-scale position value that means "fully open": 0 for
110/// ordinary devices, 100 for IoDevice::inverted ones (e.g. horizontal awnings) — the
111/// caller must resolve this from the target device, this builder does not have access
112/// to device state. Getting this wrong sends an ordinary, harmless-looking position
113/// command to the device's already-resting position instead of moving it anywhere.
114/// @return true on success.
115/// @note The elevated-priority override has not yet been confirmed against a real *active*
116/// environmental lock.
117bool create_force_open(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t open_position);
118
119/// @brief Build a position execute frame (0x00) that carries the main parameter only: the 6-byte
120/// form `originator, ACEI, MP, MP, 00, 00`, with no functional-parameter block.
121///
122/// For devices that refuse the block create_execute_position() appends. A Wisniowski / Somfy
123/// MOTO 1000 io garage opener answers that block with RESULT_PARAMETER_UNKNOWN before any
124/// challenge, accepts a 6-byte STOP, and is opened by its own 1W remote with exactly this shape
125/// (`01 43 00 00 00 00`, issue #163). See device_takes_main_parameter_only().
126/// @param f IoFrame to populate.
127/// @param own Controller's 3‑byte node ID (source address).
128/// @param dst Target device's 3‑byte node ID (destination address).
129/// @param low_power True if target is battery/solar‑powered (sets CTRL1_LOW_POWER).
130/// @param position Target position 0..100 (IO scale: 0 = open, 100 = closed).
131/// @param force_open_priority True to send at the force-open ACEI (see create_force_open()) instead
132/// of the normal one.
133/// @return true on success; false for a position above 100.
134bool create_execute_main_parameter(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t position,
135 bool force_open_priority = false);
136
137/// @brief Highest heating level low byte create_execute_heating_level() will frame (the secured level).
138static constexpr uint8_t HEATING_LEVEL_MAX = 0x3F;
139
140/// @brief Build a main-parameter-only EXECUTE (0x00) that sets a heater's heating level.
141///
142/// Same 6-byte shape as create_execute_main_parameter(), with the heating level in the main
143/// parameter (high byte HEATING_LEVEL_MP_PREFIX, low byte the level; see proto_heating_profiles.h).
144/// Experimental: unconfirmed on hardware until a capture or a reporter's heater confirms it.
145/// @param f IoFrame to populate.
146/// @param own Controller's 3-byte node ID (source address).
147/// @param dst Target device's 3-byte node ID (destination address).
148/// @param low_power True if the target is a low-power / duty-cycled device (sets CTRL1_LOW_POWER).
149/// @param level Heating level low byte (heating_level::*).
150/// @return true on success; false for a level above HEATING_LEVEL_MAX.
151bool create_execute_heating_level(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t level);
152
153/// @brief Build a main-parameter-only EXECUTE (0x00) whose main parameter is a temperature or an alias.
154///
155/// Same 6-byte shape as create_execute_heating_level(); the caller encodes the value
156/// (encode_temperature_main_parameter(), or heating_prefixed_main_parameter() for an alias).
157/// Experimental: unconfirmed on hardware.
158/// @param f IoFrame to populate.
159/// @param own Controller's 3-byte node ID (source address).
160/// @param dst Target device's 3-byte node ID (destination address).
161/// @param low_power True if the target is a low-power / duty-cycled device (sets CTRL1_LOW_POWER).
162/// @param main_parameter The 16-bit main parameter.
163/// @return true on success.
164bool create_execute_heating_main_parameter(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power,
165 uint16_t main_parameter);
166
167/// @brief Build an EXECUTE (0x00) with a temperature or alias main parameter and functional parameter 9.
168///
169/// Payload: originator, ACEI, MP(2), `00` (no FP1-FP8), `80` (FP9 present), FP9 as two bytes, high
170/// byte first (every functional-parameter value is two bytes wide, like the main parameter). FP9
171/// carries the active mode (heating_mode_flags::*). No other functional parameter is sent.
172/// Experimental: unconfirmed on hardware.
173/// @param f IoFrame to populate.
174/// @param own Controller's 3-byte node ID (source address).
175/// @param dst Target device's 3-byte node ID (destination address).
176/// @param low_power True if the target is a low-power / duty-cycled device (sets CTRL1_LOW_POWER).
177/// @param main_parameter The 16-bit main parameter.
178/// @param mode_flags FP9 value.
179/// @return true on success.
180bool create_execute_heating_with_mode_flags(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power,
181 uint16_t main_parameter, uint16_t mode_flags);
182
183/// @brief Build an EXECUTE (0x00) with a temperature or alias main parameter and functional parameter 16.
184///
185/// Payload: originator, ACEI, MP(2), `00` (no FP1-FP8), `01` (FP16 present, the last bit of the second
186/// bitmap), FP16 (two bytes, big-endian like the main parameter). No other functional parameter is sent.
187/// Experimental: unconfirmed on hardware.
188/// @param f IoFrame to populate.
189/// @param own Controller's 3-byte node ID (source address).
190/// @param dst Target device's 3-byte node ID (destination address).
191/// @param low_power True if the target is a low-power / duty-cycled device (sets CTRL1_LOW_POWER).
192/// @param main_parameter The 16-bit main parameter.
193/// @param fp16 FP16 value.
194/// @return true on success.
195bool create_execute_heating_with_fp16(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power,
196 uint16_t main_parameter, uint16_t fp16);
197
198/// @brief Build a CMD_PRIVATE (0x03) request for an arbitrary function ID.
199///
200/// function_id = 0x06 (battery-status) or 0x09 (battery-state) is reported elsewhere to read the
201/// CMD_PRIVATE_RESP reply as data[1]==0x60 => battery/solar powered, value = data[2]<<8|data[3].
202/// Unverified here -- both devices this project has probed are mains-powered and answered
203/// data[1]==0x00 (tests/corpus/captures/probe/somfy_awning_probe_private_fn_lr1121.yaml,
204/// tests/corpus/captures/probe/somfy_izymo_dimmer_probe_private_fn_lr1121.yaml).
205/// create_get_status() below is this builder frozen at function_id =
206/// PRIVATE_GET_POSITION_STATUS (0x03), the only function ID this codebase has ever captured on
207/// its own wire.
208/// @param f IoFrame to populate.
209/// @param own Controller's 3-byte node ID.
210/// @param dst Target device's 3-byte node ID.
211/// @param low_power True if the target is a low-power / duty-cycled device (sets CTRL1_LOW_POWER;
212/// the exchange layer then also uses the long wake-up preamble).
213/// @param function_id Private function ID (data[0] of the CMD_PRIVATE payload).
214/// @param sub_index Second payload byte (data[1]). Defaults to 0x00 — the only value any project
215/// has ever transmitted in this 3-byte form, and what every existing capture of ours
216/// contains. A non-zero value is a diagnostic probe into an undecoded parameter encoding:
217/// reference material describes a two-field (main parameter, functional parameter)
218/// addressing scheme here, but the field->byte mapping is **not known** — at least three
219/// encodings fit every payload observed to date equally well, because every one of them
220/// has both fields zero.
221/// @return true on success.
222bool create_private_function(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t function_id,
223 uint8_t sub_index = 0x00);
224
225// ============================================================================
226// 1W Execute (fire-and-forget class broadcast)
227// ============================================================================
228//
229// 1W has no reply and no challenge: a controller transmits once and the frame either lands or it
230// doesn't — there is no ACK to retry on and no 0x3C/0x3D to authenticate through, so the MAC
231// inside the payload (create_1w_hmac(), sequence-keyed rather than challenge-keyed) is the only
232// authentication a receiving device gets. The destination is a device *class*
233// (encode_broadcast_address(), proto_codecs.h), never an individual node — 1W has no addressed
234// unicast form at all. Both builders below are pure: they take `sequence` and `controller_key` as
235// parameters and neither increment, persist, nor look either up. The sequence store and the
236// identity registry that own those concerns (OneWayControllerRegistry, oneway_controller.h)
237// arrive in a later step; until then callers are responsible for supplying both.
238
239/// @brief Build a 1W position execute frame (CMD 0x00) targeting a device class.
240///
241/// Encodes a 0–100 position into the 2-byte main field (wire value is `2 * position`, matching
242/// the 2W numeric encoding) inside 1W's 6-byte "special" payload form — 1W uses this short form
243/// even for numeric positions, not the 8-byte layout create_execute_position() (2W) uses. The MAC
244/// span is command-specific (see create_1w_execute_command()'s doxygen); there is no default, so
245/// this builder assembles it itself via the shared internal helper.
246///
247/// @note Position 50 encodes as `2 * 50 = 0x64` on the wire, the same byte
248/// decode_1w_main_intent() labels POS_FORCE_OPEN when reading overheard traffic. That label
249/// is a decode-side diagnostic choice only (see POS_FORCE_OPEN in proto_constants.h) — it
250/// does not change what this builder sends or what a device does with it. Position 50 is an
251/// ordinary position command.
252/// @param f IoFrame to populate.
253/// @param src Our 3-byte controller node address (the 1W controller identity's `node_id`).
254/// @param target_type Device class to address; encoded via encode_broadcast_address().
255/// @param position Desired position 0–100 (0=fully open, 100=fully closed).
256/// @param sequence 2-byte rolling sequence for this transmission (big-endian on wire); the
257/// caller's identity/sequence store owns incrementing and persisting this, not this
258/// builder — see the section note above.
259/// @param controller_key 16-byte key held by the transmitting controller identity: the hub's own
260/// `system_key` for its own network, or a foreign key adopted via CMD 0x30 (Phase 3A "key
261/// adoption") when transmitting as an adopted identity.
262/// @param acei ACEI byte to place at payload[1] — `ONEWAY_EXECUTE_ACEI` (Somfy-shaped) by
263/// default, `ONEWAY_EXECUTE_ACEI_VELUX` for a VELUX identity. Resolved per identity by
264/// oneway_controller.h's effective_execute_acei(); this builder just writes it.
265/// @param broadcast_all When true, address the all-devices broadcast `00 00 3F` instead of the
266/// typed `target_type` class — what a handheld cover remote does (`execute_broadcast: all`).
267/// @return true on success; false if `position > 100` or if crypto::create_1w_hmac() fails — no
268/// partially-populated frame is left behind on failure.
269bool create_1w_execute_position(IoFrame &f, const uint8_t src[NODE_ID_SIZE], DeviceType target_type, uint8_t position,
270 uint16_t sequence, const uint8_t controller_key[AES_KEY_SIZE],
271 uint8_t acei = ONEWAY_EXECUTE_ACEI, bool broadcast_all = false);
272
273/// @brief Build a 1W named-command execute frame (CMD 0x00) targeting a device class.
274///
275/// Covers three of CoverCommand's values — STOP, FAVORITE, VENT. FORCE_OPEN is not handled here:
276/// the only wire code this project has for that label, POS_FORCE_OPEN (main=0x64), was
277/// hardware-tested as an outbound CMD_EXECUTE command and found to move a real device to 50%
278/// open, not bypass anything (see POS_FORCE_OPEN in proto_constants.h). There is no known 1W
279/// force-open encoding, so passing CoverCommand::FORCE_OPEN here falls to `default` and returns
280/// false, the same way create_execute_command()'s 2W dispatch rejects it.
281/// - STOP: main=POS_STOP (0xD2), modifier=0x00
282/// - FAVORITE: main=POS_FAVORITE (0xD8), modifier=0x00
283/// - VENT: main=POS_FAVORITE (0xD8), modifier=POS_VENT_MODIFIER (0x03)
284///
285/// @warning **These three do not rest on the same evidence, even though they read as a uniform
286/// list.** STOP is the only one a real frame pins: the published IV vector
287/// (tests/corpus/captures/oneway/reference_1w_oneway_execute_iv_vector.yaml) is a documented
288/// worked example carrying main=0xD2. VENT is not captured anywhere in this project, but it does
289/// match the reference implementation's own 1W remote byte-for-byte — its `RemoteButton::Vent`
290/// emits exactly main=0xD8/mod=0x03. FAVORITE has
291/// neither kind of support: that same reference remote has no distinct favorite/My button at all
292/// (its RemoteButton set is Open/Close/Stop/Vent/ForceOpen/Position/Absolute/Pair/Add/Remove/
293/// Mode1-4 — no Favorite), so main=0xD8/mod=0x00 here is extrapolated purely by analogy with the
294/// 2W builder's FAVORITE encoding, with no source behind it at all. Worse, the one real capture
295/// this project has of an actual My/favorite button press —
296/// tests/corpus/captures/oneway/somfy_smoove_oneway_favorite_sx1276.yaml, pinned by
297/// `OneWayCommands.FavoriteButtonCaptureIsWritePrivateNotExecute` in tests/oneway/oneway_commands_test.cpp
298/// — contradicts it directly: that remote's My button is CMD_WRITE_PRIVATE (0x20) with a 16-byte
299/// payload, not CMD_EXECUTE with main=0xD8.
300///
301/// A real enrolled device does act on main=0xD8/mod=0x00, though: it changed the brightness of a
302/// Somfy Izymo dimmer, so the byte is accepted, not ignored. What position it targets — a stored
303/// "My" position vs. some fixed value — is unconfirmed; a 2W status-poll readback would settle
304/// it. Treat FAVORITE as "does something, target unverified", not "plausibly wrong".
305///
306/// FORCE_OPEN is deliberately absent rather than merely untested. The reference remote's
307/// `RemoteButton::ForceOpen` emits main=0x64/mod=0x00 — the same bytes this project labels
308/// POS_FORCE_OPEN — so it would "match the reference implementation byte-for-byte" the same way
309/// VENT does above. But matching the reference is not evidence it works: this project
310/// hardware-tested that exact main byte as an outbound CMD_EXECUTE and confirmed it moves a real
311/// device to 50% open (see POS_FORCE_OPEN in proto_constants.h), not past any lock. There is no
312/// known 1W encoding for a real force-open, so it is unimplemented rather than shipped on a
313/// mislabeled byte.
314///
315/// The MAC span is the 7 bytes `cmd, origin, acei, main0, main1, fp1, fp2` — the command byte
316/// through fp2, stopping before the sequence, which is not frame data and never enters the MAC.
317/// Pinned by the published IV vector at
318/// tests/corpus/captures/oneway/reference_1w_oneway_execute_iv_vector.yaml and by the reference
319/// implementation's own 1W-remote span (`toAdd = 6 + 1`).
320/// This span is command-specific and does not generalise: CMD 0x30's span is `cmd + enc_key`
321/// only — see crypto::create_1w_hmac()'s `@warning`.
322/// @param f IoFrame to populate.
323/// @param src Our 3-byte controller node address (the 1W controller identity's `node_id`).
324/// @param target_type Device class to address; encoded via encode_broadcast_address().
325/// @param cmd Named command to execute (STOP, FAVORITE, or VENT). CoverCommand::FORCE_OPEN
326/// returns false — see the @warning above.
327/// @param sequence 2-byte rolling sequence for this transmission (big-endian on wire); the
328/// caller's identity/sequence store owns incrementing and persisting this, not this
329/// builder — see the section note above.
330/// @param controller_key 16-byte key held by the transmitting controller identity: the hub's own
331/// `system_key` for its own network, or a foreign key adopted via CMD 0x30 (Phase 3A "key
332/// adoption") when transmitting as an adopted identity.
333/// @param acei ACEI byte for payload[1] — see create_1w_execute_position().
334/// @param broadcast_all Address `00 00 3F` instead of the typed class — see
335/// create_1w_execute_position().
336/// @return true on success; false if `cmd` is CoverCommand::FORCE_OPEN (no 1W encoding exists) or
337/// if crypto::create_1w_hmac() fails.
338bool create_1w_execute_command(IoFrame &f, const uint8_t src[NODE_ID_SIZE], DeviceType target_type, CoverCommand cmd,
339 uint16_t sequence, const uint8_t controller_key[AES_KEY_SIZE],
340 uint8_t acei = ONEWAY_EXECUTE_ACEI, bool broadcast_all = false);
341
342// ============================================================================
343// 1W Enrollment (CMD 0x30 add-controller / CMD 0x39 remove-controller)
344// ============================================================================
345//
346// Enrollment is the precondition the two builders above assume away: a device obeys a 1W frame
347// only from a *registered* controller, so a correctly-keyed, correctly-signed execute from an
348// unknown source node is silently ignored (see ADR 0026). These two builders are how a
349// controller identity registers itself (0x30) or un-registers (0x39). Like the execute builders,
350// both are pure — `sequence` and `controller_key` are parameters, never looked up or persisted —
351// and both broadcast to the typed "all" address the same way create_1w_execute_command() does:
352// there is no addressed/unicast form, and for a virgin 1W-only device there is usually no address
353// to unicast to anyway.
354
355/// @brief Build a 1W add-controller frame (CMD 0x30) that registers this identity as a controller
356/// on every device of `target_type` currently in association mode — a physical 2 s PROG hold on
357/// the receiver, which only the device's owner can trigger (see ADR 0026).
358///
359/// The payload wraps `controller_key` for transmission rather than sending it in the clear:
360/// `enc_key = crypto::crypt_1w_key(src, controller_key)`, the same self-inverse primitive
361/// decode_1w_add_controller() uses to unwrap an overheard 0x30 (crypt_1w_key() is its own
362/// inverse, so this call and that one are literally the same function). The wrap key is the
363/// public TRANSFER_KEY
364/// (proto_constants.h) and the wrap IV derives only from `src` — see crypt_1w_key()'s doxygen for
365/// why that is not a weakness: the receiver decrypts `enc_key` first, then checks this frame's MAC
366/// under the key it just recovered, which is what makes the MAC meaningful (it proves the sender
367/// holds the key it just sent), not the wrap's secrecy.
368///
369/// Declared payload (20 bytes): `enc_key[16] + manufacturer[1] + data[1]=0x01 + sequence[2]`.
370/// **When `with_mac` is true, the MAC is an out-of-length trailer, not part of the declared
371/// payload** — CTRL0's 5-bit length field cannot express 29 declared bytes plus a 6-byte MAC
372/// together (see `IoFrame::has_mac`, `frame_carries_mac_trailer()` in proto_frame.h), so this
373/// builder sets `f.has_mac = true` and fills `f.mac[]` directly; `serialize()` appends it after
374/// the declared length and folds it into the length it returns, so a caller's CRC (computed over
375/// that return value) covers it automatically. **When present, this is the opposite placement
376/// from create_1w_remove_controller() below, whose MAC sits inside the declared payload** —
377/// copying one builder's shape to make the other is the most likely bug a future edit introduces
378/// here.
379///
380/// @warning **Real hardware and the published documentation vector disagree on whether the MAC
381/// trailer exists at all — this is why `with_mac` is a parameter, not a fixed choice.** The
382/// published `linklayer.md` vector (`with_mac=true`, 35 bytes) is what `with_mac` defaults to,
383/// for compatibility with that vector's own pinning tests. Most real hardware captures instead
384/// show 29 bytes with **no trailer at all** — see
385/// `tests/corpus/captures/enrollment/somfy_smoove_enrollment_add_and_remove_controller_sx1276.yaml` — matching
386/// the reference `_p0x30` struct (`iohcPacket.h`, no `hmac` field). **The enroll button calls
387/// this with `with_mac=false`** to match that shape, but this is a preference rather than a
388/// hardware requirement: a real Somfy Izymo dimmer has separately been shown to accept the
389/// `with_mac=true` form as well, enrolling successfully and remaining controllable afterward —
390/// both shapes are safe to send.
391///
392/// @warning Reference divergences, deliberately not followed:
393/// the iown-homecontrol project's `create_key_transfer_1w` derives the key-wrap
394/// IV from the *destination* node, which contradicts the published frame this builder is pinned
395/// against (a broadcast destination carries no identity, so `src` is the only value that
396/// reproduces it); and its comment claims "key transfer carries no MAC", which the same published
397/// frame also contradicts (its MAC is genuine and verifies) — that claim is a coincidental match
398/// for what real hardware turned out to do, not evidence the comment's reasoning was right.
399/// @param f IoFrame to populate.
400/// @param src Our 3-byte controller node address (the 1W controller identity's `node_id`) — also
401/// the key-wrap IV's only input, so this must be the identity actually being registered.
402/// @param target_type Device class to address; encoded via encode_broadcast_address(). Only
403/// devices of this class currently in association mode react.
404/// @param manufacturer Manufacturer ID byte to advertise (`man_id`); echoed back verbatim by
405/// anything that later decodes this frame with decode_1w_add_controller().
406/// @param sequence 2-byte rolling sequence for this transmission (big-endian on wire); the caller's
407/// identity/sequence store owns incrementing and persisting this, not this builder.
408/// @param controller_key 16-byte key this identity registers itself with — normally the hub's own
409/// `system_key`, wrapped here for transmission, never sent in the clear.
410/// @param with_mac True to append the out-of-length MAC trailer (the published vector's shape,
411/// and this parameter's default for backward compatibility); false to omit it entirely
412/// (the shape most real hardware captures this project holds actually use, and what the
413/// enroll button passes). Real hardware has been shown to accept both — see the
414/// `@warning` above.
415/// @return true on success; false if crypto::crypt_1w_key() fails, or (when `with_mac` is true)
416/// crypto::create_1w_hmac() fails — no partially-populated frame is left behind on failure.
417bool create_1w_add_controller(IoFrame &f, const uint8_t src[NODE_ID_SIZE], DeviceType target_type, uint8_t manufacturer,
418 uint16_t sequence, const uint8_t controller_key[AES_KEY_SIZE], bool with_mac = true);
419
420/// @brief Build a 1W remove-controller frame (CMD 0x39) that un-registers this identity from every
421/// device of `target_type` currently in association mode — the un-enroll counterpart to
422/// create_1w_add_controller(), and (per the documented `0x39` → `0x30` flow, `linklayer.md:396`)
423/// also the prelude OneWayTransmitter::send_enrollment() fires immediately before it.
424///
425/// Declared payload (9 bytes), shape matching the reference `_p0x2e` struct: `data[1]=0x00 +
426/// sequence[2] + mac[6]`. **The MAC sits inside the declared payload here** — the opposite
427/// placement from create_1w_add_controller() above, whose MAC is an out-of-length trailer because
428/// its longer payload has no room left in CTRL0's 5-bit length field. `f.has_mac` stays false.
429///
430/// The MAC span is `cmd + data` (2 bytes) — the same "everything before the sequence" shape CMD
431/// 0x00's span follows, though spans are command-specific and do not generalise on their own (see
432/// create_1w_hmac()'s `@warning`; CMD 0x30's span is `cmd + enc_key`, a different rule). This one
433/// is verified against real captures, not merely plausible: every `0x39` frame in
434/// `tests/corpus/captures/enrollment/somfy_smoove_enrollment_add_and_remove_controller_sx1276.yaml` verifies
435/// under this exact span, and `scripts/corpus/validate.py` re-checks that on every corpus
436/// validation run — a regression here would fail `make corpus-validate`, not just look wrong.
437/// @param f IoFrame to populate.
438/// @param src Our 3-byte controller node address (the 1W controller identity's `node_id`).
439/// @param target_type Device class to address; encoded via encode_broadcast_address().
440/// @param sequence 2-byte rolling sequence for this transmission (big-endian on wire); the caller's
441/// identity/sequence store owns incrementing and persisting this, not this builder.
442/// @param controller_key 16-byte key held by the identity being removed — the same key
443/// create_1w_execute_command()/create_1w_add_controller() would use for this identity.
444/// @return true on success; false if crypto::create_1w_hmac() fails — no partially-populated frame
445/// is left behind on failure.
446bool create_1w_remove_controller(IoFrame &f, const uint8_t src[NODE_ID_SIZE], DeviceType target_type, uint16_t sequence,
447 const uint8_t controller_key[AES_KEY_SIZE]);
448
449/// Build a get‑status request (0x03). The device responds with its current position.
450/// @param f IoFrame to populate.
451/// @param own Controller's 3‑byte node ID.
452/// @param dst Target device's 3‑byte node ID.
453/// @param low_power True if the target is a low-power / duty-cycled device (sets CTRL1_LOW_POWER).
454/// @return true on success.
455bool create_get_status(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power);
456
457/// @brief Build a CMD_GET_GENERAL_INFO3 (0x58) request. No payload.
458///
459/// Shares the no-payload addressed-request shape with create_get_name(), create_get_info1() and
460/// create_get_info2() via a common file-local helper (proto_commands.cpp).
461/// @param f IoFrame to populate.
462/// @param own Controller's 3-byte node ID.
463/// @param dst Target device's 3-byte node ID.
464/// @param low_power True if target is battery/solar-powered (sets CTRL1_LOW_POWER).
465/// @return true on success.
466bool create_general_info3(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power);
467
468/// @brief Build a CMD_GET_INFO1 (0x54) request. No payload.
469///
470/// The request is field-observed — a real hub sends it — but no `0x55` answer has ever been
471/// captured. Diagnostic probe only; nothing in this codebase decodes a `0x55`.
472/// @param f IoFrame to populate.
473/// @param own Controller's 3-byte node ID.
474/// @param dst Target device's 3-byte node ID.
475/// @param low_power True if target is battery/solar-powered (sets CTRL1_LOW_POWER).
476/// @return true on success.
477bool create_get_info1(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power);
478
479/// @brief Build a CMD_GET_INFO2 (0x56) request. No payload.
480///
481/// The request has **never** been captured on air; it rests on the even=request / odd=answer
482/// pairing rule, and on the fact that a `0x57` answer *is* captured (carrying a leading ASCII
483/// reference string plus the packed type/subtype bytes hub_status.cpp already reads at offsets
484/// 10/11).
485/// @param f IoFrame to populate.
486/// @param own Controller's 3-byte node ID.
487/// @param dst Target device's 3-byte node ID.
488/// @param low_power True if target is battery/solar-powered (sets CTRL1_LOW_POWER).
489/// @return true on success.
490bool create_get_info2(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power);
491
492/// Build a get-name request (0x50). The device responds with its stored display name.
493/// @param f IoFrame to populate.
494/// @param own Controller's 3-byte node ID.
495/// @param dst Target device's 3-byte node ID.
496/// @param low_power True if target is battery/solar-powered (sets CTRL1_LOW_POWER).
497/// @return true on success.
498bool create_get_name(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power);
499
500/// Build an authenticated set-name request (0x52) using a fixed zero-padded Latin-1 payload.
501/// @param f IoFrame to populate.
502/// @param own Controller's 3-byte node ID.
503/// @param dst Target device's 3-byte node ID.
504/// @param low_power True if the target is a low-power / duty-cycled device (sets CTRL1_LOW_POWER).
505/// @param payload Pre-validated fixed payload produced by encode_device_name_payload().
506/// @return true on success.
507bool create_set_name(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power,
508 const uint8_t payload[DEVICE_NAME_WRITE_PAYLOAD_SIZE]);
509
510/// @brief Build an authenticated device-identify request (0x1E) that makes a device
511/// physically identify itself (brief jog / flash).
512///
513/// @param f IoFrame to populate.
514/// @param own Controller's 3-byte node ID (source address).
515/// @param dst Target device's 3-byte node ID (destination address).
516/// @param low_power True if the target is a low-power / duty-cycled device (sets CTRL1_LOW_POWER).
517/// @note The device may reply with CMD_ERROR_RESP instead of a dedicated identify response;
518/// callers should treat that reply as an expected, non-fatal outcome rather than a failure.
519/// @return true on success.
520bool create_identify(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power);
521
522/// @brief Build a generic CMD_WRITE_PRIVATE (0x20) frame carrying a caller-supplied payload.
523///
524/// The single builder behind every heating/climate register read and write (power, setpoint, mode,
525/// presence, open-window detection, the measurement and sensor reads). The payload is produced by
526/// encode_heating_payload(), encode_heating_read() or encode_heating_write() in proto_heating.h; this
527/// builder only frames it. Framing (StartFrame=1, EndFrame=0, Protocol=0) is cross-checked against the real
528/// Atlantic Thermor exchange capture
529/// tests/corpus/captures/exchange/atlantic_thermor_exchange_write_private_param.yaml (frame 1:
530/// start=true, end=false). The device answers with CMD_WRITE_PRIVATE_ACK (0x21) after an
531/// authenticated 0x3C/0x3D leg the exchange engine handles transparently.
532/// @param f IoFrame to populate.
533/// @param own Controller's 3-byte node ID (source address).
534/// @param dst Target device's 3-byte node ID (destination address).
535/// @param low_power True if the target is a low-power / duty-cycled device (sets CTRL1_LOW_POWER;
536/// the exchange layer then also uses the long wake-up preamble). Per-device, from the
537/// target's YAML `low_power:` class (ADR 0029) — not hardcoded.
538/// @param payload Payload bytes (from encode_heating_payload(), encode_heating_read() or encode_heating_write()).
539/// @param payload_len Payload length in bytes. Rejected if 0 or > FRAME_MAX_DATA_SIZE.
540/// @return true on success; false if payload_len is out of range or set_cmd() fails.
541bool create_write_private(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, const uint8_t *payload,
542 size_t payload_len);
543
544/// Build an execute‑tilt command (0x00) for slat angle control.
545/// @param f IoFrame to populate.
546/// @param own Controller node ID.
547/// @param dst Target device node ID.
548/// @param low_power True if target is battery/solar‑powered (sets CTRL1_LOW_POWER).
549/// @param tilt_percent 0 = fully closed, 100 = fully open.
550/// @note This uses the same command (0x00) as position control but with a different
551/// payload format indicating a tilt operation. The receiver infers tilt from
552/// the payload structure. Only devices that advertise tilt support (see
553/// device_supports_tilt in proto_frame.h) will honor this.
554/// @return true on success.
555bool create_execute_tilt(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t tilt_percent);
556
557/// Build a combined position‑and‑tilt execute command (0x00).
558/// Sets both the cover position and the slat angle atomically in one frame,
559/// corresponding to the protocol's setClosureAndOrientation use case.
560/// @param f IoFrame to populate.
561/// @param own Controller node ID.
562/// @param dst Target device node ID.
563/// @param low_power True if target is battery/solar‑powered (sets CTRL1_LOW_POWER).
564/// @param position Desired position 0–100 (open→closed).
565/// @param tilt_percent 0 = fully closed, 100 = fully open.
566/// @return true on success; false if position exceeds limits.
567bool create_execute_position_and_tilt(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power,
568 uint8_t position, uint8_t tilt_percent);
569
570/// @brief Build an extended CMD_PRIVATE (0x03) request with a selector/block pair.
571///
572/// The shape real hubs use for both the tilt block (selector STATUS_TILT_SELECTOR: FPI1 bit 5 = FP3,
573/// see proto_constants.h) and the
574/// field-observed selector 0x80 (tests/corpus/captures/probe/multi_somfy_probe_extended_private_both_selectors.yaml),
575/// which this codebase has never decoded. create_get_status_tilt() below is this builder frozen at selector =
576/// STATUS_TILT_SELECTOR, block = 0x01.
577/// @param f IoFrame to populate.
578/// @param own Controller's 3-byte node ID.
579/// @param dst Target device's 3-byte node ID.
580/// @param low_power True if the target is a low-power / duty-cycled device (sets CTRL1_LOW_POWER).
581/// @param selector Extended-status selector byte (data[1]).
582/// @param block Selector-specific block/index byte (data[2]) — the field-observed name for
583/// this byte is "N" for selector 0x80, where the corpus has only ever observed 0x00/0x01.
584/// @param function_id CMD_PRIVATE function ID at data[0]. Defaults to PRIVATE_GET_POSITION_STATUS
585/// (0x03) — the only value ever observed on air in this 4-byte extended shape. Other values
586/// are diagnostic probes: the *shape* `03 80 00 00` / `03 80 01 00` is field-observed from
587/// a real hub, but no other function ID has ever been seen in it.
588/// @return true on success.
589bool create_get_status_extended(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t selector,
590 uint8_t block, uint8_t function_id = PRIVATE_GET_POSITION_STATUS);
591
592/// Build the status read of the main parameter plus functional parameters FP1-FP7: an
593/// unauthenticated CMD_PRIVATE (0x03) request with function ID 0x01, FPI1 mask 0xFE, one
594/// "current value" status type per parameter and FPI2 0x00 — the exact 10 bytes a VELUX
595/// KLF 200 gateway sends (public reference: KLF 200 API specification v3.18 §10.3.1.4; the same
596/// bytes were heard on air from a live KLF 200 at 868 MHz and 2.4 GHz). The reply layout is not
597/// decoded here.
598/// @param f IoFrame to populate.
599/// @param own Controller's 3-byte node ID.
600/// @param dst Target device's 3-byte node ID.
601/// @param low_power True if the target is a low-power / duty-cycled device (sets CTRL1_LOW_POWER).
602/// @return true on success.
603bool create_status_mp_fp_read(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power);
604
605/// Build the read of a device's resulting limitation: an unauthenticated CMD_LIMITATION_STATUS_REQ
606/// (0x25) with data `{selector, 0x00, 0x00}`, byte for byte what a VELUX KLF 200 gateway was
607/// heard sending (issue #98; public reference: KLF 200 API specification v3.18 section 10.5). The
608/// reply's value layout is confirmed under rain on a VELUX window (minimum 93 %); its originator and
609/// remaining-time bytes read 00 even then, so they stay unverified. Read-only: the
610/// selector is an enum with two values, so this builder cannot produce a limitation write.
611/// @param f IoFrame to populate.
612/// @param own Controller's 3-byte node ID.
613/// @param dst Target device's 3-byte node ID.
614/// @param type Which resulting limit to read.
615/// @param low_power True if the target is a low-power / duty-cycled device (sets CTRL1_LOW_POWER).
616/// @return true on success.
617bool create_limitation_status_read(IoFrame &f, const uint8_t *own, const uint8_t *dst, LimitationType type,
618 bool low_power);
619
620/// Build a tilt‑aware get‑status request (0x03 with extended payload) that returns
621/// the 16‑byte tilt block in the response.
622/// @param f IoFrame to populate.
623/// @param own Controller node ID.
624/// @param dst Target device node ID.
625/// @param low_power True if the target is a low-power / duty-cycled device (sets CTRL1_LOW_POWER).
626/// @return true on success.
627bool create_get_status_tilt(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power);
628
629/// @brief Build a CMD_PRIVATE2 (0x0C) request in either of the two field-observed shapes.
630///
631/// The payload is CMD_EXECUTE's POS_FAVORITE/POS_VENT_MODIFIER stored-position selector with
632/// the execution prefix stripped. `low_power` is an explicit parameter like every other
633/// device-addressed builder in this file, not derived from `long_form`: the two captures this
634/// builder is pinned against happen to carry CTRL1_LOW_POWER set on the long-form request and
635/// clear on the short-form one, but that tracks each capture's *target device's* power class
636/// (solar shutter vs. mains switch), not the payload shape — see proto_commands.cpp for the
637/// full reasoning.
638/// @param f IoFrame to populate.
639/// @param own Controller's 3-byte node ID.
640/// @param dst Target device's 3-byte node ID.
641/// @param modifier The POS_FAVORITE/POS_VENT_MODIFIER-family selector byte to read back.
642/// @param long_form True for the 6-byte long form (data = POS_UNKNOWN, 0x00, 0x80,
643/// POS_FAVORITE, modifier, 0x00); false for the 4-byte short form (data = POS_FAVORITE,
644/// modifier, 0x00, 0x00).
645/// @param low_power True if target is battery/solar-powered (sets CTRL1_LOW_POWER).
646/// @return true on success.
647bool create_private2_read(IoFrame &f, const uint8_t *own, const uint8_t *dst, uint8_t modifier, bool long_form,
648 bool low_power);
649
650/// @brief Build a discovery request with configurable command, destination, and payload.
651///
652/// Supports the command codes 0x28 (DISCOVER_REQ), 0x2A (DISCOVER_SPE_REQ), and
653/// 0x2E (DISCOVER_ALT_REQ, alternate discovery). For 0x2A the payload is a 6-byte random nonce
654/// followed by a 6-byte HMAC computed over the command byte alone, using the nonce as the
655/// challenge and the supplied system key.
656///
657/// @param f IoFrame to populate.
658/// @param own Controller's 3-byte node ID.
659/// @param command Discovery command code (0x28, 0x2A, or 0x2E).
660/// @param dst Destination node ID (broadcast or explicit).
661/// @param low_power True to set the LOW_POWER flag in CTRL1.
662/// @param ack_capable True to set the ACK flag (CTRL1_ACK) in CTRL1 for this one broadcast. Each
663/// caller decides per frame: the `pairing_discovery_ack_capable` tuning knob gates it for
664/// Discover & Pair's discovery broadcast (opt-in, default off — see the init_frame()
665/// doc in proto_frame.h for why CTRL1_ACK must never become an unconditional default), and
666/// the SPE roll-call's low-power frame shape (`CTRL1 = LOW_POWER | ACK`) always passes
667/// true.
668/// @param payload_enabled True when the optional payload byte is enabled.
669/// @param payload Optional payload byte (only used when command requires a payload).
670/// @param system_key 16-byte system key; only used for 0x2A HMAC computation.
671/// @return true on success; false for unsupported command or missing key for 0x2A.
672bool create_discovery_request(IoFrame &f, const uint8_t *own, uint8_t command, const uint8_t *dst, bool low_power,
673 bool ack_capable, bool payload_enabled, uint8_t payload, const uint8_t *system_key);
674
675/// Build a discovery broadcast (0x28). Sent to the broadcast address; only devices
676/// in pairing mode (PROG button pressed) will respond.
677/// @param f IoFrame to populate.
678/// @param own Controller node ID.
679/// @note Destination is BROADCAST_DISCOVER (0x00003B). The device responds with
680/// CMD_DISCOVER_RESP (0x29) containing its node ID and type/subtype. The
681/// controller then switches to point‑to‑point communication for phases 2 and 3.
682/// @return true on success.
683bool create_discover(IoFrame &f, const uint8_t *own);
684
685/// @brief Build a discovery response (0x29) — the device side of discovery, used by the
686/// key-extraction responder (see pairing_responder.h) to emulate an unpaired device.
687///
688/// Almost every builder in this file speaks the *controller* side of the protocol; this one,
689/// create_key_confirm(), create_discover_confirm_ack(), and create_challenge_req_device_role()
690/// below speak the *device* side, needed only for that one reverse-role feature. Device-side
691/// frames never set CTRL1_LOW_POWER: that bit describes the *target* of a controller-originated
692/// frame, and a device's replies are addressed to a mains-powered hub.
693/// Payload layout matches the full 9-byte discovery response format documented at
694/// DISCOVERY_RESP_BACKBONE_OFFSET/_MANUFACTURER_OFFSET/_FLAGS_OFFSET/_TIMESTAMP_OFFSET
695/// (proto_constants.h), cross-checked against a real Somfy actuator's captured 0x29
696/// (tests/corpus/captures/discovery/somfy_awning_discovery_lab_response.yaml): backbone address
697/// equals the device's own node ID, start+end set, low_power clear.
698/// @param f IoFrame to populate.
699/// @param own Our advertised (throwaway) node ID — used as both src and the backbone address.
700/// @param dst Destination node ID (the discovering hub's real node ID, from its 0x28's src).
701/// @param type Device type to advertise.
702/// @param subtype Device subtype to advertise.
703/// @param manufacturer_id Manufacturer ID to advertise (see MANUFACTURER_* in proto_constants.h).
704/// @note The flags byte (turnaround class, power-save) and timestamp are best-effort placeholder
705/// values — ATT_CLASS_40MS/POWER_SAVE_LOW_POWER and a non-zero timestamp, matching a real
706/// captured device rather than the honest-but-misleading ATT_CLASS_5MS/POWER_SAVE_ALWAYS_ALIVE
707/// (0x00) — see KEY_EXTRACTION_DISCOVER_RESP_FLAGS/_TIMESTAMP in proto_commands.cpp for the
708/// full reasoning. Still unverified against a real hub.
709/// @return true on success.
710bool create_discover_resp(IoFrame &f, const uint8_t *own, const uint8_t *dst, DeviceType type, uint8_t subtype,
711 uint8_t manufacturer_id);
712
713/// @brief Build a key-confirm frame (0x33) — the device's acknowledgement that it received and
714/// installed the system key, sent after decrypting a CMD_KEY_TRANSFER (0x32).
715///
716/// Device-side counterpart to create_key_transfer(); used only by the key-extraction responder
717/// (see create_discover_resp() above for why this direction exists at all). No payload and END
718/// set, matching real devices' 0x33 in
719/// tests/corpus/captures/pairing/somfy_izymo_dimmer_pairing_full_sx1276.yaml,
720/// tests/corpus/captures/pairing/velux_kux100_pairing_full.yaml, and (this project's own key-extraction responder
721/// against a real hub) tests/corpus/captures/pairing/velux_kig300_pairing_key_extraction_success.yaml — 0x33 closes the
722/// key-exchange sequence that CMD_KEY_INIT (0x31) opened with START.
723/// @param f IoFrame to populate.
724/// @param own Our advertised (throwaway) node ID.
725/// @param dst Destination node ID (the hub that sent the key transfer).
726/// @return true on success.
727bool create_key_confirm(IoFrame &f, const uint8_t *own, const uint8_t *dst);
728
729/// @brief Build a discovery-confirm request (0x2C) — sent by a controller directly to a
730/// freshly-discovered device, before proceeding to the key exchange (0x31).
731///
732/// Every real controller in this project's corpus sends 0x2C between the device's discovery
733/// response (0x29) and its own key-init (0x31): KLR200→KUX100
734/// (tests/corpus/captures/pairing/velux_kux100_pairing_full.yaml), TaHoma→VELUX SSL
735/// (tests/corpus/captures/discovery/velux_ssl_discovery_tahoma_pairing.yaml), and this project's
736/// own key-extraction responder answering KIG300/KLR200/KLR300/Somfy TaHoma/Somfy Connectivity
737/// Kit (tests/corpus/captures/pairing/velux_kig300_pairing_key_extraction_success.yaml and
738/// siblings). Every one of those real hubs sets CTRL1_ACK for an always-alive target (`48 10`) and
739/// clears it for a low-power one (`48 20`, CTRL1_LOW_POWER only, no ACK) — see the `ack` param doc
740/// for what this builder does with that split. Gated by the `pairing_discover_confirm` tuning knob
741/// (`DiscoverConfirmMode::SKIP`/`SEND`/`SEND_WITH_ACK`, tuning_config.h) — `skip` reproduces the
742/// sequence without this step: no 0x2C, no post-step pause.
743/// @param f IoFrame to populate.
744/// @param own Controller's 3-byte node ID.
745/// @param dst The freshly-discovered device's 3-byte node ID (from its 0x29's src).
746/// @param low_power Set CTRL1_LOW_POWER. See this header's "Low-power flag and preamble
747/// handling" note above: unlike most pairing builders, this one is NOT a fixed value —
748/// pass the device's own self-reported power-save class from discovery
749/// (`pairing::PairingContext::discovery_low_power`).
750/// @param ack Set CTRL1_ACK ("sender can handle 2W responses"). The builder is literal — it does
751/// not derive `ack` from `low_power` itself; that mirror rule belongs to the caller
752/// (`PairingEngine::run_discover_confirm_step_()`), which never asks for `ack` when
753/// `low_power` is true. Every corpus hub's own 0x2C sets CTRL1_ACK for an always-alive
754/// target (`48 10`) and clears it for a low-power one (`48 20`); this codebase's `send`
755/// default instead clears it for an always-alive target too (`48 00`, no corpus hub sends
756/// that shape, but a Somfy Izymo dimmer answers it with 0x2D and ignores `48 10`), and
757/// `send_with_ack` switches to the hub-observed `48 10`.
758/// @return true on success.
759bool create_discover_confirm(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, bool ack);
760
761/// @brief Build a discovery-confirm acknowledgement (0x2D) — the device's answer to a hub's
762/// CMD_DISCOVER_CONFIRM (0x2C), which a hub sends directly to a freshly-discovered device before
763/// it will proceed to the key exchange.
764///
765/// Device-side only, like create_discover_resp()/create_key_confirm() above. Its controller-role
766/// counterpart is create_discover_confirm() just above.
767/// No payload and END set, matching real devices' 0x2D in
768/// tests/corpus/captures/pairing/velux_kux100_pairing_full.yaml; for a second independent hub,
769/// tests/corpus/captures/pairing/somfy_connectivity_kit_pairing_key_extraction_stall.yaml, where
770/// an already-paired device answers the same hub the key-extraction responder was talking to; and
771/// for this exact builder exercised against a real hub,
772/// tests/corpus/captures/pairing/velux_kig300_pairing_key_extraction_success.yaml.
773/// @param f IoFrame to populate.
774/// @param own Our advertised (throwaway) node ID.
775/// @param dst Destination node ID (the hub that sent the discovery confirm).
776/// @return true on success.
777bool create_discover_confirm_ack(IoFrame &f, const uint8_t *own, const uint8_t *dst);
778
779/// @brief Recover the system key from an inbound CMD_KEY_TRANSFER (0x32) payload — the decode
780/// counterpart to create_key_transfer()'s encode.
781///
782/// Centralizes the IV-`data` convention in one place: create_key_transfer() derives its IV from
783/// the *preceding* CMD_KEY_INIT (0x31) command byte only (see its own doxygen), so decoding must
784/// use that same single-byte `{CMD_KEY_INIT}` — not the 0x32 frame's own command byte, and not
785/// the discovery frame. crypt_key() is symmetric, so this is the same primitive in reverse.
786/// @param transfer_payload 16-byte CMD_KEY_TRANSFER payload (frame.data).
787/// @param challenge The 6-byte challenge *we* generated and sent in our own CMD_CHALLENGE_REQ
788/// (0x3C) — the far side mixes this into its IV, so decoding requires the exact same bytes.
789/// @param out_key Output: recovered 16-byte system key.
790/// @return true on success (crypt_key() AES failure is the only false case).
791bool recover_system_key_from_transfer(const uint8_t transfer_payload[AES_KEY_SIZE], const uint8_t challenge[HMAC_SIZE],
792 uint8_t out_key[AES_KEY_SIZE]);
793
794/// @brief Build a key transfer request (0x38, "pull") asking a discovered device for its key.
795///
796/// START set, END clear, CTRL1_LOW_POWER clear, CTRL1 bit 2 (CTRL1_PRIORITY) set: the shape vendor
797/// controllers use for this request. An Atlantic AGILIA IO radiator answers it with a CMD_KEY_TRANSFER
798/// (0x32) (issue #165); recover_key_from_pull() decrypts that.
799/// @param f IoFrame to populate.
800/// @param own Controller node ID.
801/// @param dst Discovered device node ID.
802/// @param challenge 6 random bytes, kept by the caller for the decrypt.
803/// @return true on success.
804bool create_launch_key_transfer(IoFrame &f, const uint8_t *own, const uint8_t *dst, const uint8_t challenge[HMAC_SIZE]);
805
806/// @brief Recover a device's key from the CMD_KEY_TRANSFER (0x32) it sent in answer to our
807/// CMD_LAUNCH_KEY_TRANSFER (0x38).
808///
809/// Same construction as recover_system_key_from_transfer(), with the IV taken from the request
810/// that asked for the key: its `data` is the whole request, `{CMD_LAUNCH_KEY_TRANSFER, r0..r5}`, and
811/// its challenge the request's 6 bytes. Checked against a documented pull exchange in
812/// proto_crypto_test.cpp, and on an Atlantic AGILIA IO radiator, whose 0x3D proved the key (issue #165).
813/// @param request Our 0x38, as built by create_launch_key_transfer().
814/// @param transfer_payload 16-byte CMD_KEY_TRANSFER payload from the device.
815/// @param out_key Output: the device's 16-byte key.
816/// @return true on success; false when `request` carries fewer than 6 bytes or AES fails.
817bool recover_key_from_pull(const IoFrame &request, const uint8_t transfer_payload[AES_KEY_SIZE],
818 uint8_t out_key[AES_KEY_SIZE]);
819
820/// Build a key‑init request (0x31) to start pairing key exchange with a discovered device.
821/// @param f IoFrame to populate.
822/// @param own Controller node ID.
823/// @param dst Discovered device node ID.
824/// @return true on success.
825bool create_key_init(IoFrame &f, const uint8_t *own, const uint8_t *dst);
826
827/// Build a key‑transfer frame (0x32) containing the system key encrypted with the transfer key.
828/// @param f IoFrame to populate.
829/// @param old_frame The key‑init frame (used to derive the encryption IV).
830/// @param dst Target device node ID.
831/// @param src Controller node ID.
832/// @param key The 16‑byte system key to transfer.
833/// @param challenge 6‑byte challenge received from device in its 0x3C response.
834/// @note The system key is obfuscated via the XOR‑AES construction in crypt_key().
835/// The transfer key (hardcoded in proto_frame.h) is the same for all IO‑Homecontrol
836/// devices worldwide; its purpose is to protect the system key in transit during
837/// initial pairing. Once transferred, the device uses the system key for all
838/// subsequent authenticated exchanges.
839/// @return true on success.
840bool create_key_transfer(IoFrame &f, IoFrame &old_frame, const uint8_t *dst, const uint8_t *src,
841 const uint8_t key[AES_KEY_SIZE], const uint8_t challenge[HMAC_SIZE]);
842
843/// Build a challenge request (0x3C) containing 6 random bytes. Used when we need to
844/// authenticate an incoming request from a device.
845/// @param f IoFrame to populate.
846/// @param dst Target device node ID (device we're challenging).
847/// @param src Controller node ID.
848/// @return true on success.
849bool create_challenge_req(IoFrame &f, const uint8_t *dst, const uint8_t *src);
850
851/// @brief Build a challenge request (0x3C) using a caller-supplied challenge instead of
852/// generating a fresh one internally.
853///
854/// The no-challenge overload above generates its own random bytes and does not expose them,
855/// which is fine for the normal inbound-auth path (the challenge is only ever needed once, to
856/// build this same frame). A caller that needs the *exact* bytes again later — the key-extraction
857/// responder decrypting the corresponding CMD_KEY_TRANSFER (0x32) — generates the challenge itself
858/// and passes it in here, keeping the transmitted 0x3C and the later decrypt on one source of
859/// truth. Note that responder uses create_challenge_req_device_role() below, not this overload.
860/// @param f IoFrame to populate.
861/// @param dst Target device node ID (device we're challenging).
862/// @param src Our own node ID.
863/// @param challenge Caller-supplied 6-byte challenge (e.g. from crypto::generate_challenge()).
864/// @return true on success.
865bool create_challenge_req(IoFrame &f, const uint8_t *dst, const uint8_t *src, const uint8_t challenge[HMAC_SIZE]);
866
867/// @brief Build a challenge request (0x3C) in the *device* direction — used only by the
868/// key-extraction responder to challenge a foreign hub that sent us CMD_KEY_INIT (0x31).
869///
870/// Same command and payload as the controller-role builders above, but framed the way a real
871/// device frames it: START clear and LOW_POWER clear. Both controller-role overloads set both
872/// bits, which is correct for their direction — LOW_POWER describes the *target* of a
873/// controller-originated frame (a device that may be battery/solar powered, see this header's
874/// convention note), and the controller's 0x3C opens its own inbound-auth exchange. Neither holds
875/// for a device answering a hub's key-init: real devices' pairing 0x3C frames in
876/// tests/corpus/captures/pairing/somfy_izymo_dimmer_pairing_full_sx1276.yaml (`0E 00 …`) and
877/// tests/corpus/captures/pairing/velux_kux100_pairing_full.yaml carry neither bit, because the frame is a continuation
878/// of the hub's already-open exchange and is addressed to a mains-powered hub — and this exact builder, exercised
879/// against a real hub, produces the identical `0E 00` shape in
880/// tests/corpus/captures/pairing/velux_kig300_pairing_key_extraction_success.yaml.
881/// @param f IoFrame to populate.
882/// @param dst The foreign hub's node ID (from the inbound 0x31's src).
883/// @param src Our advertised (throwaway) node ID.
884/// @param challenge Caller-supplied 6-byte challenge, retained for the later 0x32 decrypt.
885/// @return true on success.
886bool create_challenge_req_device_role(IoFrame &f, const uint8_t *dst, const uint8_t *src,
887 const uint8_t challenge[HMAC_SIZE]);
888
889/// Build a challenge response (0x3D) proving we know the system key.
890/// HMAC is computed over [original_command_id + original_data] using the challenge.
891/// @param f IoFrame to populate.
892/// @param dst Target device node ID.
893/// @param src Controller node ID.
894/// @param challenge 6‑byte challenge from the device.
895/// @param origin Original request frame that triggered the challenge.
896/// @param key System key (16 bytes).
897/// @note The HMAC derivation uses the challenge as IV salt; see create_hmac() in
898/// proto_crypto.h for the exact construction. This frame authenticates the
899/// controller to the device for the current exchange.
900/// @return true on success.
901bool create_challenge_resp(IoFrame &f, const uint8_t *dst, const uint8_t *src, const uint8_t challenge[HMAC_SIZE],
902 const IoFrame &origin, const uint8_t *key);
903
904/// Compute the 6-byte MAC a challenge response (0x3D) carries for `origin`: create_hmac() over the
905/// challenged frame's command byte and payload. Used for our own 0x3D (create_challenge_resp()).
906/// @param origin The challenged frame.
907/// @param challenge 6-byte challenge from the 0x3C.
908/// @param key 16-byte key.
909/// @param hmac Output: 6-byte MAC.
910/// @return true on success.
911bool challenge_resp_hmac(const IoFrame &origin, const uint8_t challenge[HMAC_SIZE], const uint8_t *key,
912 uint8_t hmac[HMAC_SIZE]);
913
914/// Check a device's challenge response (0x3D) for `origin`, the frame it sent and we challenged.
915/// The device signs its own frame, the same transcript as challenge_resp_hmac().
916/// @param origin The device's challenged frame.
917/// @param challenge The 6 bytes we sent in the 0x3C.
918/// @param mac The 6 bytes of the device's 0x3D.
919/// @param key 16-byte key the MAC must verify under.
920/// @return true if the MAC matches.
921bool verify_challenge_resp(const IoFrame &origin, const uint8_t challenge[HMAC_SIZE], const uint8_t mac[HMAC_SIZE],
922 const uint8_t *key);
923
924/// @brief Build a node verification response (0x37) — device side, answering a hub's CMD_NODE_VERIFY_REQ
925/// (0x36).
926///
927/// Payload is our own advertised node ID — the only identity this emulated device has to offer —
928/// the same value create_discover_resp() reports at DISCOVERY_RESP_BACKBONE_OFFSET.
929/// CorpusDeviceRoleBuilders.NodeVerifyRespPayloadMatchesOwnDiscoverRespBackboneAddress
930/// (tests/corpus/corpus_device_role_builder_test.cpp) pins that these two builders agree with *each
931/// other*, not that this matches a real device's own backbone value: the one real capture of this
932/// exchange (tests/corpus/captures/pairing/velux_kux100_pairing_full.yaml) shows a genuine device whose 0x37
933/// payload — a persistent identity the io-homecontrol wire format tracks separately from a device's
934/// node/session address — does NOT equal that device's own node ID. Our emulated device only ever
935/// generates one identity per arm cycle, so it structurally cannot reproduce a real device's
936/// separate backbone value; whether any real hub requires the two to differ, or even inspects this
937/// field at all rather than treating it as informational, is unconfirmed. See
938/// docs/key-extraction.md's "Known limitations" for the field-facing version of this note.
939/// Also unverified: the full CTRL1 framing. This builder's ctrl1 is 0x00 (see init_frame() call in
940/// the .cpp), but the KLR200 capture's 0x37 carries CTRL1_PRIORITY set. See the
941/// TODO(hardware-verify) on the .cpp definition for why that bit is not mirrored here.
942/// No controller-role counterpart exists to share with: this codebase has never sent 0x36, so there
943/// is no analogous controller-role code that receives a 0x37 to keep in sync with.
944/// @param f IoFrame to populate.
945/// @param own Our advertised (throwaway) node ID — used as both src and the payload.
946/// @param dst Destination node ID (the hub that sent the node verification request, from its 0x36's src).
947/// @return true on success.
948bool create_node_verify_resp_device_role(IoFrame &f, const uint8_t *own, const uint8_t *dst);
949
950/// @brief Build a challenge response (0x3D) in the *device* direction — used only by the
951/// key-extraction responder to answer a hub-issued CMD_CHALLENGE_REQ (0x3C) challenging our own
952/// CMD_NODE_VERIFY_RESP (0x37).
953///
954/// Same transcript rule as create_challenge_resp() above (the challenged party HMACs its own
955/// preceding frame's cmd+data), but END is set: this 0x3D closes the node-verification round the
956/// hub opened with 0x36 (tests/corpus/captures/pairing/velux_kux100_pairing_full.yaml's `8E 08 …`, END
957/// set). LOW_POWER is clear because CTRL1_LOW_POWER describes a controller-originated frame's
958/// *target*, not the sender — irrelevant here, since this is a device-role frame sent to a
959/// mains-powered hub. Unrelated to create_discover_resp()'s own Multi Information Byte, which as of
960/// KEY_EXTRACTION_DISCOVER_RESP_FLAGS (proto_commands.cpp) deliberately advertises
961/// POWER_SAVE_LOW_POWER for this responder's own (different) power-save self-description — the two
962/// bits answer different questions and are not expected to match. Neither
963/// bit is a blanket "device role" convention — real devices also send mid-exchange 0x3D frames with
964/// END clear and LOW_POWER set (somfy_oximo40_statuspoll_sx1262.yaml) — so this framing is
965/// specific to the terminal shape this feature needs, not a general device-role rule.
966/// @param f IoFrame to populate.
967/// @param dst The hub's node ID (from the inbound 0x3C's src).
968/// @param src Our advertised (throwaway) node ID.
969/// @param challenge 6-byte challenge from the hub's 0x3C.
970/// @param origin Our own preceding CMD_NODE_VERIFY_RESP (0x37) frame — its cmd+data is the transcript.
971/// @param key System key (16 bytes).
972/// @return true on success.
973bool create_challenge_resp_device_role(IoFrame &f, const uint8_t *dst, const uint8_t *src,
974 const uint8_t challenge[HMAC_SIZE], const IoFrame &origin, const uint8_t *key);
975
976/// Build a status‑update acknowledgment (0x72). Sent after authenticating a device's
977/// status update; broadcast on all 3 channels for reliability.
978/// @param f IoFrame to populate.
979/// @param own Controller node ID.
980/// @param dst Device node ID that sent the update.
981/// @return true on success.
982bool create_status_update_resp(IoFrame &f, const uint8_t *own, const uint8_t *dst);
983
984/// Build a set‑config command (0x6F) telling the device to automatically send
985/// status updates when controlled by any remote.
986/// @param f IoFrame to populate.
987/// @param own Controller node ID.
988/// @param dst Target device node ID.
989/// @note This configures the device to emit CMD_STATUS_UPDATE (0x71) frames whenever
990/// it is controlled by any remote (including the paired controller). This enables
991/// HA to receive unsolicited position updates. The controller must still
992/// authenticate the status update using the inbound auth flow (hub_exchange.h).
993/// @todo Confirm on real hardware which device families actually honor this SetConfig1
994/// payload and emit unsolicited status updates after pairing.
995/// @return true on success.
996bool create_set_config1(IoFrame &f, const uint8_t *own, const uint8_t *dst);
997
998} // namespace home_io_control
999} // namespace esphome
bool create_force_open(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t open_position)
Build a force-open execute frame (0x00): an ordinary position command to the device's wire-scale "ful...
bool create_identify(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build an authenticated device-identify request (0x1E).
bool create_get_name(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build a get-name request (0x50).
bool create_get_status(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build a get-status request (0x03). The device responds with its current position.
bool create_set_name(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, const uint8_t payload[DEVICE_NAME_WRITE_PAYLOAD_SIZE])
Build an authenticated set-name request (0x52) using a fixed zero-padded Latin-1 payload.
bool create_write_private(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, const uint8_t *payload, size_t payload_len)
Build a generic CMD_WRITE_PRIVATE (0x20) frame around a caller-supplied payload — the one builder beh...
bool create_node_verify_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_private2_read(IoFrame &f, const uint8_t *own, const uint8_t *dst, uint8_t modifier, bool long_form, bool low_power)
Build a CMD_PRIVATE2 (0x0C) request in either of the two field-observed shapes.
bool verify_challenge_resp(const IoFrame &origin, const uint8_t challenge[HMAC_SIZE], const uint8_t mac[HMAC_SIZE], const uint8_t *key)
Check a device's 0x3D for origin, in constant time (crypto::verify_hmac()).
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 create_limitation_status_read(IoFrame &f, const uint8_t *own, const uint8_t *dst, LimitationType type, bool low_power)
Build the limitation read (see the header for the evidence and the read-only guarantee).
CoverCommand
Named device commands for cover-type actuators.
bool create_1w_remove_controller(IoFrame &f, const uint8_t src[NODE_ID_SIZE], DeviceType target_type, uint16_t sequence, const uint8_t controller_key[AES_KEY_SIZE])
Build a 1W remove-controller frame (CMD 0x39).
bool recover_system_key_from_transfer(const uint8_t transfer_payload[AES_KEY_SIZE], const uint8_t challenge[HMAC_SIZE], uint8_t out_key[AES_KEY_SIZE])
Recover the system key from a CMD_KEY_TRANSFER payload.
bool create_execute_heating_level(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t level)
Build a heating-level execute frame (0x00). See proto_commands.h.
bool create_discover_confirm(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, bool ack)
Build a discovery-confirm request (0x2C) — controller side, sent directly to a freshly-discovered dev...
bool create_get_info1(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build a CMD_GET_INFO1 (0x54) request. No payload. See proto_commands.h for the evidence note.
DeviceType
IO-Homecontrol node types.
bool create_execute_position_and_tilt(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t position, uint8_t tilt_percent)
Build a combined position-and-tilt execute command (0x00) — setClosureAndOrientation.
bool create_1w_execute_command(IoFrame &f, const uint8_t src[NODE_ID_SIZE], DeviceType target_type, CoverCommand cmd, uint16_t sequence, const uint8_t controller_key[AES_KEY_SIZE], uint8_t acei, bool broadcast_all)
Build a 1W named-command execute frame (CMD 0x00) targeting a device class.
static constexpr uint8_t HEATING_LEVEL_MAX
Highest heating level low byte create_execute_heating_level() will frame (the secured level).
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.
bool create_execute_tilt(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t tilt_percent)
Build a tilt execute command (0x00) for devices that support slat angle control.
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_execute_heating_main_parameter(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint16_t main_parameter)
Build a temperature / alias execute frame (0x00), main parameter only. See proto_commands....
bool create_challenge_resp(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 challenge response (0x3D) proving we know the system key.
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.
bool create_execute_position(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t position, bool silent)
Build a position execute command (0x00) to move a device to a numeric position.
bool create_general_info3(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build a CMD_GET_GENERAL_INFO3 (0x58) request.
bool create_execute_heating_with_fp16(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint16_t main_parameter, uint16_t fp16)
Build a temperature / alias execute frame (0x00) carrying functional parameter 16....
bool create_status_mp_fp_read(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build the KLF-style status read of the main parameter plus FP1-FP7 (see the header for the evidence).
bool create_get_info2(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build a CMD_GET_INFO2 (0x56) request. No payload. See proto_commands.h for the evidence note.
bool create_execute_command(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, CoverCommand cmd, bool silent)
Build a named-command execute frame (0x00) for STOP, FAVORITE, or VENT.
bool challenge_resp_hmac(const IoFrame &origin, const uint8_t challenge[HMAC_SIZE], const uint8_t *key, uint8_t hmac[HMAC_SIZE])
The 0x3D MAC for origin.
bool recover_key_from_pull(const IoFrame &request, const uint8_t transfer_payload[AES_KEY_SIZE], uint8_t out_key[AES_KEY_SIZE])
Recover a device's key from its answer to our 0x38.
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...
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...
bool create_discover(IoFrame &f, const uint8_t *own)
Build a discovery broadcast (0x28).
bool create_private_function(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t function_id, uint8_t sub_index)
Build a CMD_PRIVATE (0x03) request for an arbitrary function ID.
bool create_discovery_request(IoFrame &f, const uint8_t *own, uint8_t command, const uint8_t *dst, bool low_power, bool ack_capable, bool payload_enabled, uint8_t payload, const uint8_t *system_key)
Build a configurable discovery request command (0x28, 0x2A, or 0x2E).
bool create_get_status_tilt(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build a tilt-aware get-status request (0x03) that returns the extended 16-byte tilt payload.
bool create_get_status_extended(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t selector, uint8_t block, uint8_t function_id)
Build an extended CMD_PRIVATE (0x03) request with a selector/block pair — the shape real hubs use for...
LimitationType
On-air selector byte (request data[0]) choosing which resulting limit to read.
bool create_launch_key_transfer(IoFrame &f, const uint8_t *own, const uint8_t *dst, const uint8_t challenge[HMAC_SIZE])
Build a key transfer request (0x38). See proto_commands.h for the frame shape.
bool create_execute_heating_with_mode_flags(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint16_t main_parameter, uint16_t mode_flags)
Build a temperature / alias execute frame (0x00) carrying functional parameter 9. See proto_commands....
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...
bool create_1w_execute_position(IoFrame &f, const uint8_t src[NODE_ID_SIZE], DeviceType target_type, uint8_t position, uint16_t sequence, const uint8_t controller_key[AES_KEY_SIZE], uint8_t acei, bool broadcast_all)
Build a 1W position execute frame (CMD 0x00) targeting a device class.
bool create_challenge_req(IoFrame &f, const uint8_t *dst, const uint8_t *src, const uint8_t challenge[HMAC_SIZE])
Build a challenge request (0x3C) using a caller-supplied challenge.
bool create_execute_main_parameter(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t position, bool force_open_priority)
Build a main-parameter-only position execute frame (0x00). See proto_commands.h.
bool create_status_update_resp(IoFrame &f, const uint8_t *own, const uint8_t *dst)
Build a status-update acknowledgment (0x72).
bool create_1w_add_controller(IoFrame &f, const uint8_t src[NODE_ID_SIZE], DeviceType target_type, uint8_t manufacturer, uint16_t sequence, const uint8_t controller_key[AES_KEY_SIZE], bool with_mac)
Build a 1W add-controller frame (CMD 0x30).
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.
Device-name, address-classification and 1W-frame codecs.
IO-Homecontrol command IDs, result codes and protocol enumerations.
IO-Homecontrol device-type model, capabilities and runtime device state.
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:93