Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
proto_commands.cpp
Go to the documentation of this file.
1/// @file proto_commands.cpp
2/// @brief Command builders for the IO-Homecontrol protocol.
3/// @ingroup hioc_protocol
4
5#include "proto_commands.h"
6
7#include "proto_constants.h"
8#include "proto_crypto.h"
9
10#include <array>
11#include <cstring>
12
13namespace esphome {
14namespace home_io_control {
15
16namespace {
17
18// === Command payload templates ===
19
20/// The protocol uses 0-100 for percentage-style position inputs before encoding them on wire.
21constexpr uint8_t POSITION_PERCENT_MAX = 100;
22/// Byte 0 in execute-family payloads identifies a user-originated remote action.
23/// Uses the public ORIGINATOR_USER_REMOTE constant from proto_frame.h.
24constexpr uint8_t EXECUTE_ORIGINATOR = ORIGINATOR_USER_REMOTE;
25/// ACEI byte for execute commands — composed from priority and validity bits.
26/// Level=2 (user_high) matches real IO-homecontrol remotes and avoids
27/// RESULT_PRIORITY_LOCKED_NON_EXEC (0x38) rejections on devices locked at level 3.
28/// Composition: (ACEI_LEVEL_USER_HIGH << 5) | (0 << 3) | (1 << 1) | 1 = 0x43.
29constexpr uint8_t EXECUTE_ACEI =
31/// @brief ACEI byte for the force-open command — same bit layout as EXECUTE_ACEI but at the
32/// highest priority level instead of user_high.
33///
34/// The protocol's only documented override mechanism for an environmental soft lock (e.g. a
35/// wind/rain sensor holding a device at its secured position) is ACEI priority elevation: a node
36/// locked at some priority level rejects any command at that level or below (result codes
37/// RESULT_PRIORITY_LEVEL_LOCKED / RESULT_PRIORITY_LOCKED_NON_EXEC) and only a strictly
38/// higher-priority command gets through. Real captures of this repo's own wind/rain sensor
39/// traffic show it locks at ACEI_LEVEL_PROTECTION_SENSOR (1), so force-open uses level 0
40/// (protection_human, the highest level)
41/// Composition: (ACEI_LEVEL_PROTECTION_HUMAN << 5) | (1 << 1) | 1 = 0x03.
42/// @note Real-hardware testing confirmed this correctly moves a device to fully open (see
43/// create_force_open()'s position-inversion note), but elevation to level 0 has not yet
44/// been confirmed to actually override an *active* lock — only that the device accepts
45/// the frame when nothing is locking it.
46constexpr uint8_t EXECUTE_ACEI_FORCE_OPEN =
48/// Standard payload length for full execute-family commands.
49constexpr size_t EXECUTE_PAYLOAD_SIZE = 8;
50/// Bit flag that marks the standard position payload layout after the encoded position byte.
51constexpr uint8_t EXECUTE_POSITION_LAYOUT_FLAG = 0x80;
52/// Controller-capture matched helper byte used in normal execute payloads.
53constexpr uint8_t EXECUTE_POSITION_PROFILE = 0x06;
54/// Short payload length for special execute commands such as stop/favorite.
55constexpr size_t EXECUTE_SPECIAL_PAYLOAD_SIZE = 6;
56/// Private sub-command for position status requests.
57constexpr uint8_t PRIVATE_GET_POSITION_STATUS = 0x03;
58/// Status-update acknowledgement payload matched from controller traffic.
59constexpr uint8_t STATUS_UPDATE_ACK_PAYLOAD[] = {0x05, 0x00};
60/// Set-config payload that enables automatic status updates from the device.
61constexpr uint8_t SET_CONFIG1_STATUS_BROADCAST_PAYLOAD[] = {0xE0, 0x10, 0x0A, 0x08, 0x00};
62/// Identify-request parameter byte (data[1] of the CMD_IDENTIFY payload).
63constexpr uint8_t IDENTIFY_PARAMETER = 0xFF;
64
65/// @brief Build the standard 8-byte position payload shared by create_execute_position() and
66/// create_force_open() — identical except for the ACEI byte.
67inline std::array<uint8_t, EXECUTE_PAYLOAD_SIZE> make_position_payload(uint8_t acei, uint8_t position) {
68 return {EXECUTE_ORIGINATOR, acei, static_cast<uint8_t>(2 * position), 0x00,
69 EXECUTE_POSITION_LAYOUT_FLAG, POS_FAVORITE, EXECUTE_POSITION_PROFILE, 0x00};
70}
71
72} // namespace
73
74/// Build a position execute command (0x00) to move a device to a numeric position.
75bool create_execute_position(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t position) {
76 if (position > POSITION_PERCENT_MAX)
77 return false;
78 init_frame(f, true, true, false, low_power);
79 set_dst(f, dst);
80 set_src(f, own);
81 const auto payload = make_position_payload(EXECUTE_ACEI, position);
82 return set_cmd(f, CMD_EXECUTE, payload.data(), payload.size());
83}
84
85/// Build a named-command execute frame (0x00) for STOP, FAVORITE, or VENT.
86///
87/// FORCE_OPEN is deliberately not handled here — unlike these three, it needs to know the
88/// device's wire-scale "fully open" position (0 or 100 depending on IoDevice::inverted, e.g.
89/// horizontal awnings), which this builder has no way to know. Use create_force_open() instead;
90/// see its comments for why.
91bool create_execute_command(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, CoverCommand cmd) {
92 uint8_t main_byte = 0;
93 uint8_t modifier_byte = 0;
94 switch (cmd) {
96 main_byte = POS_STOP;
97 modifier_byte = 0x00;
98 break;
100 main_byte = POS_FAVORITE;
101 modifier_byte = 0x00;
102 break;
104 main_byte = POS_FAVORITE;
105 modifier_byte = POS_VENT_MODIFIER;
106 break;
107 default:
108 return false;
109 }
110 init_frame(f, true, true, false, low_power);
111 set_dst(f, dst);
112 set_src(f, own);
113 const uint8_t payload[EXECUTE_SPECIAL_PAYLOAD_SIZE] = {EXECUTE_ORIGINATOR, EXECUTE_ACEI, main_byte,
114 modifier_byte, 0x00, 0x00};
115 return set_cmd(f, CMD_EXECUTE, payload, sizeof(payload));
116}
117
118/// Build a force-open execute frame (0x00): an ordinary position command to the device's
119/// wire-scale "fully open" value, sent at elevated ACEI priority (see EXECUTE_ACEI_FORCE_OPEN).
120///
121/// Takes the target position explicitly rather than assuming 0, because "fully open" is not
122/// always wire-position 0: IoDevice::inverted devices (e.g. horizontal awnings) have open/close
123/// swapped, so their fully-open wire position is 100. An earlier version of this builder
124/// hardcoded 0; on a real inverted awning that targeted its already-*closed* resting position, a
125/// real-hardware-confirmed no-op rather than a lock bypass. The caller (execute_device_command_()
126/// in hub_operations.cpp) is responsible for resolving the correct value from the target IoDevice.
127bool create_force_open(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t open_position) {
128 init_frame(f, true, true, false, low_power);
129 set_dst(f, dst);
130 set_src(f, own);
131 const auto payload = make_position_payload(EXECUTE_ACEI_FORCE_OPEN, open_position);
132 return set_cmd(f, CMD_EXECUTE, payload.data(), payload.size());
133}
134
135/// Build a get-status request (0x03). The device responds with its current position.
136bool create_get_status(IoFrame &f, const uint8_t *own, const uint8_t *dst) {
137 // low_power=true for solar devices.
138 init_frame(f, true, true, false, true);
139 set_dst(f, dst);
140 set_src(f, own);
141 // Private sub-command = get position status.
142 uint8_t d[3] = {PRIVATE_GET_POSITION_STATUS, 0x00, 0x00};
143 return set_cmd(f, CMD_PRIVATE, d, sizeof(d));
144}
145
146bool create_get_name(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power) {
147 init_frame(f, true, true, false, low_power);
148 set_dst(f, dst);
149 set_src(f, own);
150 return set_cmd(f, CMD_GET_NAME);
151}
152
153bool create_set_name(IoFrame &f, const uint8_t *own, const uint8_t *dst,
154 const uint8_t payload[DEVICE_NAME_WRITE_PAYLOAD_SIZE]) {
155 // low_power=true: matches every other frame addressed to a specific device (see
156 // create_get_status/create_key_init) — solar/battery devices need CTRL1_LOW_POWER set on
157 // every frame sent to them, not only the initiating request.
158 init_frame(f, true, true, false, true);
159 set_dst(f, dst);
160 set_src(f, own);
162}
163
164/// Build an authenticated device-identify request (0x1E).
165bool create_identify(IoFrame &f, const uint8_t *own, const uint8_t *dst) {
166 // low_power=true (see create_set_name above).
167 init_frame(f, true, true, false, true);
168 set_dst(f, dst);
169 set_src(f, own);
170 const uint8_t payload[2] = {ORIGINATOR_USER_REMOTE, IDENTIFY_PARAMETER};
171 return set_cmd(f, CMD_IDENTIFY, payload, sizeof(payload));
172}
173
174/// Build a tilt execute command (0x00) for devices that support slat angle control.
175bool create_execute_tilt(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t tilt_percent) {
176 init_frame(f, true, true, false, low_power);
177 set_dst(f, dst);
178 set_src(f, own);
179
180 auto const tilt_value =
181 static_cast<uint16_t>((POSITION_PERCENT_MAX - tilt_percent) * STATUS_POS_MAX / POSITION_PERCENT_MAX);
182 uint8_t d[EXECUTE_PAYLOAD_SIZE] = {EXECUTE_ORIGINATOR,
183 EXECUTE_ACEI,
185 0x00,
187 static_cast<uint8_t>(tilt_value >> BITS_PER_BYTE),
188 static_cast<uint8_t>(tilt_value),
189 0x00};
190 return set_cmd(f, CMD_EXECUTE, d, sizeof(d));
191}
192
193/// Build a combined position-and-tilt execute command (0x00) — setClosureAndOrientation.
194bool create_execute_position_and_tilt(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power,
195 uint8_t position, uint8_t tilt_percent) {
196 if (position > POSITION_PERCENT_MAX)
197 return false;
198 init_frame(f, true, true, false, low_power);
199 set_dst(f, dst);
200 set_src(f, own);
201
202 auto const tilt_value =
203 static_cast<uint16_t>((POSITION_PERCENT_MAX - tilt_percent) * STATUS_POS_MAX / POSITION_PERCENT_MAX);
204 uint8_t d[EXECUTE_PAYLOAD_SIZE] = {EXECUTE_ORIGINATOR,
205 EXECUTE_ACEI,
206 static_cast<uint8_t>(2 * position),
207 0x00,
209 static_cast<uint8_t>(tilt_value >> BITS_PER_BYTE),
210 static_cast<uint8_t>(tilt_value),
211 0x00};
212 return set_cmd(f, CMD_EXECUTE, d, sizeof(d));
213}
214
215/// Build a tilt-aware get-status request (0x03) that returns the extended 16-byte tilt payload.
216bool create_get_status_tilt(IoFrame &f, const uint8_t *own, const uint8_t *dst) {
217 init_frame(f, true, true, false, true);
218 set_dst(f, dst);
219 set_src(f, own);
220 // The selector byte switches the private status response to the extended tilt layout.
221 uint8_t d[4] = {PRIVATE_GET_POSITION_STATUS, STATUS_TILT_SELECTOR, 0x01, 0x00};
222 return set_cmd(f, CMD_PRIVATE, d, sizeof(d));
223}
224
225/// Build a discovery broadcast (0x28). Sent to the broadcast address 0x00003B.
226/// Only devices in pairing mode (PROG button pressed) will respond.
227bool create_discover(IoFrame &f, const uint8_t *own) {
228 // start+end: single broadcast frame.
229 init_frame(f, true, true, true, false);
231 set_src(f, own);
232 return set_cmd(f, CMD_DISCOVER_REQ);
233}
234
235/// Build a configurable discovery request command (0x28, 0x2A, or 0x2E).
236///
237/// For 0x2A (Discover SPE), the payload is a 6-byte random nonce followed by a 6-byte
238/// HMAC over the command byte alone, using that nonce as the challenge and the supplied
239/// system key — one frame carrying a whole challenge-response, which is what lets a
240/// broadcast be authenticated. This requires a valid system key; it will not work for a
241/// motor that has never been paired with this controller's key.
242bool create_discovery_request(IoFrame &f, const uint8_t *own, uint8_t command, const uint8_t *dst, bool low_power,
243 bool payload_enabled, uint8_t payload, const uint8_t *system_key) {
244 init_frame(f, true, true, true, low_power);
245 set_dst(f, dst);
246 set_src(f, own);
247
248 switch (command) {
249 case CMD_DISCOVER_REQ:
250 return set_cmd(f, CMD_DISCOVER_REQ);
251
253 if (system_key == nullptr)
254 return false;
255 uint8_t nonce[HMAC_SIZE];
257 // The HMAC covers the command byte *alone*, with the nonce as the challenge — not the
258 // nonce as transcript data, which is what this built until real bytes settled it (a Velux
259 // KLR200's own 0x2A, tests/corpus/captures/velux_kux100/pairing_full.yaml, recomputed
260 // under that installation's key). The old [cmd, nonce] transcript produced an HMAC no
261 // device could verify, so every 0x2A we emitted was silently unanswerable.
262 uint8_t hmac[HMAC_SIZE];
263 if (!crypto::create_hmac(&command, 1, nonce, system_key, hmac))
264 return false;
265 uint8_t payload_buf[HMAC_SIZE * 2];
266 memcpy(payload_buf, nonce, HMAC_SIZE);
267 memcpy(payload_buf + HMAC_SIZE, hmac, HMAC_SIZE);
268 return set_cmd(f, CMD_DISCOVER_SPE_REQ, payload_buf, sizeof(payload_buf));
269 }
270
272 // 0x2E may carry an optional single-byte payload (e.g., 0x00) or be sent with no payload.
273 if (payload_enabled)
274 return set_cmd(f, CMD_DISCOVER_ALT_REQ, &payload, 1);
275 return set_cmd(f, CMD_DISCOVER_ALT_REQ);
276 }
277
278 default:
279 return false;
280 }
281}
282
283/// Build a discovery response (0x29) — device side, used only by the key-extraction responder.
284/// See proto_commands.h for the full contract and the real-capture cross-check.
285bool create_discover_resp(IoFrame &f, const uint8_t *own, const uint8_t *dst, DeviceType type, uint8_t subtype,
286 uint8_t manufacturer_id) {
287 init_frame(f, true, true, true, false);
288 set_dst(f, dst);
289 set_src(f, own);
290
291 uint8_t payload[DISCOVERY_RESP_FULL_SIZE] = {0};
292 encode_packed_device_type(type, subtype, payload[0], payload[1]);
293 // Backbone address: the captured real Somfy 0x29 (see proto_commands.h doxygen) reports the
294 // device's own node ID here, so we mirror that rather than inventing a separate address.
295 memcpy(&payload[DISCOVERY_RESP_BACKBONE_OFFSET], own, NODE_ID_SIZE);
296 payload[DISCOVERY_RESP_MANUFACTURER_OFFSET] = manufacturer_id;
297 // Flags: turnaround class ATT_CLASS_5S (0) and POWER_SAVE_ALWAYS_ALIVE (0) both encode to 0 —
298 // an honest description of this responder (it never sleeps while armed). Best-effort; see
299 // proto_commands.h @note.
300 // TODO(hardware-verify): confirm a real hub accepts these flags/timestamp values, or whether
301 // it requires specific ATT/power-save/timestamp semantics before completing pairing.
302 payload[DISCOVERY_RESP_FLAGS_OFFSET] = 0x00;
303 payload[DISCOVERY_RESP_TIMESTAMP_OFFSET] = 0x00;
304 payload[DISCOVERY_RESP_TIMESTAMP_OFFSET + 1] = 0x00;
305 return set_cmd(f, CMD_DISCOVER_RESP, payload, sizeof(payload));
306}
307
308/// Build a bare device→hub terminal acknowledgement: no payload, END set, START and LOW_POWER
309/// clear. Shared by create_key_confirm() and create_discover_confirm_ack(), which are the same
310/// frame shape and differ only in command byte — real captures of both
311/// (tests/corpus/captures/somfy_dimmer/pairing_full.yaml's 0x33 `88 00 …`,
312/// velux_kux100/pairing_full.yaml's 0x2D `88 08 …`, and this project's own key-extraction
313/// responder against a real hub in
314/// tests/corpus/captures/issues/issue_45_velux_kig300_key_extraction_success.yaml, both 0x2D and
315/// 0x33 as `88 00 …`) show a device closing its half of a two-frame handshake this way. LOW_POWER
316/// stays clear because that bit describes the *target* of a controller-originated frame (see the
317/// header's convention note); a device does not flag a frame it sends *to* the hub as low-power.
318static bool create_device_terminal_ack(IoFrame &f, const uint8_t *own, const uint8_t *dst, uint8_t cmd) {
319 init_frame(f, true, false, true, false);
320 set_dst(f, dst);
321 set_src(f, own);
322 return set_cmd(f, cmd);
323}
324
325/// Build a key-confirm frame (0x33) — device side, used only by the key-extraction responder.
326/// See proto_commands.h for the full contract and the real-capture cross-check.
327bool create_key_confirm(IoFrame &f, const uint8_t *own, const uint8_t *dst) {
328 return create_device_terminal_ack(f, own, dst, CMD_KEY_CONFIRM);
329}
330
331/// Build a discovery-confirm acknowledgement (0x2D) — device side, used only by the
332/// key-extraction responder. See proto_commands.h for the full contract.
333bool create_discover_confirm_ack(IoFrame &f, const uint8_t *own, const uint8_t *dst) {
335}
336
337/// Recover the system key from a CMD_KEY_TRANSFER payload. See proto_commands.h for the full
338/// contract; this is the single place the IV-`data` convention (`{CMD_KEY_INIT}, len 1`) lives
339/// for the decode direction, mirroring create_key_transfer()'s encode side below.
340bool recover_system_key_from_transfer(const uint8_t transfer_payload[AES_KEY_SIZE], const uint8_t challenge[HMAC_SIZE],
341 uint8_t out_key[AES_KEY_SIZE]) {
342 const uint8_t key_init_cmd = CMD_KEY_INIT;
343 return crypto::crypt_key(&key_init_cmd, 1, challenge, transfer_payload, out_key);
344}
345
346/// Build a key-init request (0x31) to start the pairing key exchange with a discovered device.
347bool create_key_init(IoFrame &f, const uint8_t *own, const uint8_t *dst) {
348 init_frame(f, true, true, false, true);
349 set_dst(f, dst);
350 set_src(f, own);
351 return set_cmd(f, CMD_KEY_INIT);
352}
353
354/// Build a key-transfer frame (0x32) containing the system key encrypted with the transfer key.
355bool create_key_transfer(IoFrame &f, IoFrame &old_frame, const uint8_t *dst, const uint8_t *src,
356 const uint8_t key[AES_KEY_SIZE], const uint8_t challenge[HMAC_SIZE]) {
357 // low_power=true: see create_set_name above.
358 init_frame(f, true, false, false, true);
359 set_dst(f, dst);
360 set_src(f, src);
361 // The pairing capture we matched derives the IV from the previous command byte only. Treating
362 // the key-init frame that narrowly keeps our key transfer aligned with real controllers.
363 uint8_t enc_key[AES_KEY_SIZE];
364 if (!crypto::crypt_key(&old_frame.cmd, 1, challenge, key, enc_key))
365 return false;
366 return set_cmd(f, CMD_KEY_TRANSFER, enc_key, AES_KEY_SIZE);
367}
368
369/// Build a challenge request (0x3C) with caller-chosen framing bits. Shared by the
370/// controller-role and device-role builders below, which differ only in those bits.
371static bool create_challenge_req_framed(IoFrame &f, const uint8_t *dst, const uint8_t *src,
372 const uint8_t challenge[HMAC_SIZE], bool start, bool low_power) {
373 init_frame(f, true, start, false, low_power);
374 set_dst(f, dst);
375 set_src(f, src);
376 return set_cmd(f, CMD_CHALLENGE_REQ, challenge, HMAC_SIZE);
377}
378
379/// Build a challenge request (0x3C) using a caller-supplied challenge. See proto_commands.h.
380/// low_power is set because the target is a device that may be battery/solar powered (see
381/// create_set_name above).
382bool create_challenge_req(IoFrame &f, const uint8_t *dst, const uint8_t *src, const uint8_t challenge[HMAC_SIZE]) {
383 return create_challenge_req_framed(f, dst, src, challenge, /*start=*/true, /*low_power=*/true);
384}
385
386/// Build a challenge request (0x3C) containing 6 random bytes.
387/// Used when WE need to authenticate an incoming request from a device.
388bool create_challenge_req(IoFrame &f, const uint8_t *dst, const uint8_t *src) {
389 uint8_t challenge[HMAC_SIZE];
391 return create_challenge_req(f, dst, src, challenge);
392}
393
394/// Build a device-role challenge request (0x3C) — device side, used only by the key-extraction
395/// responder. See proto_commands.h for why the framing bits differ from the controller-role
396/// builders above.
397bool create_challenge_req_device_role(IoFrame &f, const uint8_t *dst, const uint8_t *src,
398 const uint8_t challenge[HMAC_SIZE]) {
399 return create_challenge_req_framed(f, dst, src, challenge, /*start=*/false, /*low_power=*/false);
400}
401
402/// Build a challenge response (0x3D) proving we know the system key.
403/// The HMAC is computed over [original_command_id + original_data] using the challenge.
404bool create_challenge_resp(IoFrame &f, const uint8_t *dst, const uint8_t *src, const uint8_t challenge[HMAC_SIZE],
405 const IoFrame &origin, const uint8_t *key) {
406 // low_power=true (see create_set_name above)
407 init_frame(f, true, false, false, true);
408 set_dst(f, dst);
409 set_src(f, src);
410 // The authenticated transcript covers the original request, not the 0x3D wrapper. Using the
411 // origin command byte and payload here was one of the key interoperability findings.
412 uint8_t frame_data[FRAME_MAX_SIZE];
413 frame_data[0] = origin.cmd;
414 memcpy(frame_data + 1, origin.data, origin.data_len);
415 uint8_t hmac[HMAC_SIZE];
416 if (!crypto::create_hmac(frame_data, origin.data_len + 1, challenge, key, hmac))
417 return false;
418 return set_cmd(f, CMD_CHALLENGE_RESP, hmac, HMAC_SIZE);
419}
420
421/// Build a status-update acknowledgment (0x72). Sent after authenticating a device's status update.
422/// The response is sent on all 3 channels to ensure the device receives it.
423bool create_status_update_resp(IoFrame &f, const uint8_t *own, const uint8_t *dst) {
424 // end=true: final frame. low_power=true (see create_set_name above).
425 init_frame(f, true, false, true, true);
426 set_dst(f, dst);
427 set_src(f, own);
428 // Status update acknowledgment payload matched from working controller captures.
429 return set_cmd(f, CMD_STATUS_UPDATE_RESP, STATUS_UPDATE_ACK_PAYLOAD, sizeof(STATUS_UPDATE_ACK_PAYLOAD));
430}
431
432/// Build a set-config command (0x6F) to tell the device to automatically send status updates
433/// when controlled by any remote (not just us). Not all devices support this.
434bool create_set_config1(IoFrame &f, const uint8_t *own, const uint8_t *dst) {
435 // low_power=true (see create_set_name above).
436 init_frame(f, true, true, false, true);
437 set_dst(f, dst);
438 set_src(f, own);
439 // Set-config payload matched from working controller captures.
440 return set_cmd(f, CMD_SET_CONFIG1, SET_CONFIG1_STATUS_BROADCAST_PAYLOAD,
441 sizeof(SET_CONFIG1_STATUS_BROADCAST_PAYLOAD));
442}
443
444} // namespace home_io_control
445} // namespace esphome
void generate_challenge(uint8_t out[HMAC_SIZE])
Generate 6 random bytes for a challenge using the ESP32 hardware RNG.
bool create_hmac(const uint8_t *data, uint8_t len, const uint8_t challenge[HMAC_SIZE], const uint8_t key[AES_KEY_SIZE], uint8_t hmac[HMAC_SIZE])
Create a 6-byte HMAC for authentication (proprietary IO-Homecontrol scheme).
bool crypt_key(const uint8_t *data, uint8_t len, const uint8_t challenge[HMAC_SIZE], const uint8_t in[AES_KEY_SIZE], uint8_t out[AES_KEY_SIZE])
Encrypt or decrypt a system key during pairing.
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 set_cmd(IoFrame &f, uint8_t cmd, const uint8_t *params, uint8_t params_len)
Set command and payload.
static constexpr uint8_t ACEI_LEVEL_USER_HIGH
High-priority user controller.
bool create_get_name(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build a get-name request (0x50).
void encode_packed_device_type(DeviceType type, uint8_t subtype, uint8_t &type_msb, uint8_t &type_subtype)
Encode a DeviceType/subtype pair into the two-byte packed metadata format used by discovery responses...
static constexpr uint8_t BITS_PER_BYTE
Number of bits in one protocol byte.
Definition proto_sizes.h:27
static bool create_device_terminal_ack(IoFrame &f, const uint8_t *own, const uint8_t *dst, uint8_t cmd)
Build a bare device→hub terminal acknowledgement: no payload, END set, START and LOW_POWER clear.
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").
Definition proto_sizes.h:20
static constexpr uint8_t CMD_SET_CONFIG1
Configure device to auto-send status updates.
static constexpr uint16_t STATUS_POS_MAX
In status responses, position is encoded as a 16-bit value where 0x0000 = fully open (0%) and 0xC800 ...
static constexpr uint8_t CMD_KEY_TRANSFER
Send encrypted system key to device.
static constexpr uint8_t CMD_DISCOVER_CONFIRM_ACK
Device acknowledges confirmation.
DeviceType
Device type identifiers reported by IO‑Homecontrol products.
bool create_get_status(IoFrame &f, const uint8_t *own, const uint8_t *dst)
Build a get-status request (0x03). The device responds with its current position.
static constexpr uint8_t DISCOVERY_RESP_MANUFACTURER_OFFSET
Manufacturer ID at data[5].
static constexpr uint8_t POS_UNKNOWN
Wire value: position unknown / keep current.
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.
CoverCommand
Named device commands for cover-type actuators.
@ FAVORITE
Move to stored favorite/"My" position.
@ VENT
Move to ventilation position (window-type devices).
bool create_discovery_request(IoFrame &f, const uint8_t *own, uint8_t command, const uint8_t *dst, bool low_power, bool payload_enabled, uint8_t payload, const uint8_t *system_key)
Build a configurable discovery request command (0x28, 0x2A, or 0x2E).
static bool create_challenge_req_framed(IoFrame &f, const uint8_t *dst, const uint8_t *src, const uint8_t challenge[HMAC_SIZE], bool start, bool low_power)
Build a challenge request (0x3C) with caller-chosen framing bits.
static constexpr uint8_t CMD_DISCOVER_ALT_REQ
Alternate discovery.
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.
static constexpr uint8_t CMD_GET_NAME
Request device name.
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.
static constexpr uint8_t HMAC_SIZE
Authentication HMAC is 6 bytes (truncated AES output).
Definition proto_sizes.h:22
static constexpr uint8_t ACEI_EXTENDED_SHIFT
Shift for extended field extraction.
void init_frame(IoFrame &f, bool is_2w, bool start, bool end, bool low_power)
Initialize an IoFrame header (ctrl0/ctrl1) with flags.
static constexpr uint8_t DISCOVERY_RESP_BACKBONE_OFFSET
Byte offsets within CMD_DISCOVER_RESP (0x29) payload data.
static constexpr uint8_t FRAME_MAX_SIZE
Maximum frame size (9 header + 23 data).
Definition proto_sizes.h:30
static constexpr uint8_t DEVICE_NAME_WRITE_PAYLOAD_SIZE
Fixed write payload: 15 visible chars plus trailing null/padding.
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_CONFIRM
Device confirms key was received.
static constexpr uint8_t ACEI_LEVEL_PROTECTION_HUMAN
ACEI priority level values (0–7).
static constexpr uint8_t CMD_KEY_INIT
Initiate key transfer to device.
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_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.
void set_dst(IoFrame &f, const uint8_t id[NODE_ID_SIZE])
Set destination node ID.
static constexpr uint8_t POS_VENT_MODIFIER
Modifier byte for the ventilation command.
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.
static constexpr uint8_t DISCOVERY_RESP_FLAGS_OFFSET
Flags byte at data[6].
static constexpr uint8_t CMD_DISCOVER_SPE_REQ
Broadcast roll-call answered by every device that already holds this controller's system key,...
static constexpr uint8_t CMD_EXECUTE
Set position/open/close/stop — requires authentication.
static constexpr uint8_t CMD_CHALLENGE_REQ
6-byte random challenge.
static constexpr uint8_t CMD_STATUS_UPDATE_RESP
Acknowledge status update.
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...
static constexpr uint8_t CMD_SET_NAME
Set device name (authenticated).
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 ACEI_LEVEL_SHIFT
Shift for priority level extraction.
bool create_discover(IoFrame &f, const uint8_t *own)
Build a discovery broadcast (0x28).
static constexpr uint8_t POS_FAVORITE
Wire value: move to favorite/"My" position.
static constexpr uint8_t BROADCAST_DISCOVER[NODE_ID_SIZE]
Broadcast address for device discovery (0x00003B).
static constexpr uint8_t CMD_DISCOVER_RESP
Device responds with its ID and type.
static constexpr uint8_t DISCOVERY_RESP_FULL_SIZE
Full discovery response payload size.
static constexpr uint8_t DISCOVERY_RESP_TIMESTAMP_OFFSET
Timestamp starts at data[7] (2 bytes).
bool create_identify(IoFrame &f, const uint8_t *own, const uint8_t *dst)
Build an authenticated device-identify request (0x1E).
bool create_set_name(IoFrame &f, const uint8_t *own, const uint8_t *dst, const uint8_t payload[DEVICE_NAME_WRITE_PAYLOAD_SIZE])
Build an authenticated set-name request (0x52) using a fixed zero-padded Latin-1 payload.
static constexpr uint8_t STATUS_TILT_SELECTOR
Extended status payload marker for tilt-capable devices.
static constexpr uint8_t CMD_IDENTIFY
Device physical identification / jog — requires authentication.
bool create_execute_position(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t position)
Build a position execute command (0x00) to move a device to a numeric position.
static constexpr uint8_t CMD_PRIVATE
Get device status — no authentication needed.
static constexpr uint8_t AES_KEY_SIZE
AES-128 key size.
Definition proto_sizes.h:23
bool create_execute_command(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, CoverCommand cmd)
Build a named-command execute frame (0x00) for STOP, FAVORITE, or VENT.
static constexpr uint8_t POS_STOP
Position values in the IO protocol.
static constexpr uint8_t ORIGINATOR_USER_REMOTE
User sent command from a remote control.
static constexpr uint8_t CMD_CHALLENGE_RESP
HMAC proof answering a 0x3C.
bool create_get_status_tilt(IoFrame &f, const uint8_t *own, const uint8_t *dst)
Build a tilt-aware get-status request (0x03) that returns the extended 16-byte tilt payload.
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_status_update_resp(IoFrame &f, const uint8_t *own, const uint8_t *dst)
Build a status-update acknowledgment (0x72).
static constexpr uint8_t ACEI_VALID_BIT
ACEI byte bit-field definitions.
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.
void set_src(IoFrame &f, const uint8_t id[NODE_ID_SIZE])
Set source node ID.
Command builders for the IO‑Homecontrol protocol.
IO-Homecontrol command IDs, result codes and protocol enumerations.
Cryptographic helpers for the IO‑Homecontrol protocol.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:71
uint8_t data[FRAME_MAX_DATA_SIZE]
Command parameters (0–23 bytes).
Definition proto_frame.h:77
uint8_t data_len
Actual length of data.
Definition proto_frame.h:78