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_crypto.h"
8
9#include <cstring>
10
11namespace esphome {
12namespace home_io_control {
13
14namespace {
15
16// === Command payload templates ===
17
18/// The protocol uses 0-100 for percentage-style position inputs before encoding them on wire.
19constexpr uint8_t POSITION_PERCENT_MAX = 100;
20/// Byte 0 in execute-family payloads identifies a user-originated remote action.
21constexpr uint8_t EXECUTE_USER_ORIGINATOR = 0x01;
22/// ACEI/profile byte observed on normal execute commands.
23constexpr uint8_t EXECUTE_POSITION_ACEI = 0x67;
24/// Standard payload length for full execute-family commands.
25constexpr size_t EXECUTE_PAYLOAD_SIZE = 8;
26/// Bit flag that marks the standard position payload layout after the encoded position byte.
27constexpr uint8_t EXECUTE_POSITION_LAYOUT_FLAG = 0x80;
28/// Controller-capture matched helper byte used in normal execute payloads.
29constexpr uint8_t EXECUTE_POSITION_PROFILE = 0x06;
30/// ACEI/profile byte observed on tilt execute commands.
31constexpr uint8_t EXECUTE_TILT_ACEI = 0xE7;
32/// Short payload length for special execute commands such as stop/favorite.
33constexpr size_t EXECUTE_SPECIAL_PAYLOAD_SIZE = 6;
34/// Private sub-command for position status requests.
35constexpr uint8_t PRIVATE_GET_POSITION_STATUS = 0x03;
36/// Status-update acknowledgement payload matched from controller traffic.
37constexpr uint8_t STATUS_UPDATE_ACK_PAYLOAD[] = {0x05, 0x00};
38/// Set-config payload that enables automatic status updates from the device.
39constexpr uint8_t SET_CONFIG1_STATUS_BROADCAST_PAYLOAD[] = {0xE0, 0x10, 0x0A, 0x08, 0x00};
40
41} // namespace
42
43/// Build an execute command (0x00) to control a device.
44/// For real positions (0-100), the value is doubled in the frame (0x00=0%, 0xC8=100%).
45/// For special commands (stop/favorite), a shorter 6-byte payload is used.
46bool create_execute(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t position) {
47 init_frame(f, true, true, false, low_power);
48 set_dst(f, dst);
49 set_src(f, own);
50 if (position <= POSITION_PERCENT_MAX) {
51 // Real position: doubled value (0-200 maps to 0-100%).
52 const uint8_t payload[EXECUTE_PAYLOAD_SIZE] = {
53 EXECUTE_USER_ORIGINATOR, EXECUTE_POSITION_ACEI, static_cast<uint8_t>(2 * position), 0x00,
54 EXECUTE_POSITION_LAYOUT_FLAG, POS_FAVORITE, EXECUTE_POSITION_PROFILE, 0x00};
55 return set_cmd(f, CMD_EXECUTE, payload, sizeof(payload));
56 }
57
58 // Special command (stop=0xD2, favorite=0xD8).
59 const uint8_t payload[EXECUTE_SPECIAL_PAYLOAD_SIZE] = {
60 EXECUTE_USER_ORIGINATOR, EXECUTE_POSITION_ACEI, position, 0x00, 0x00, 0x00};
61 return set_cmd(f, CMD_EXECUTE, payload, sizeof(payload));
62}
63
64/// Build a get-status request (0x03). The device responds with its current position.
65bool create_get_status(IoFrame &f, const uint8_t *own, const uint8_t *dst) {
66 // low_power=true for solar devices.
67 init_frame(f, true, true, false, true);
68 set_dst(f, dst);
69 set_src(f, own);
70 // Private sub-command = get position status.
71 uint8_t d[3] = {PRIVATE_GET_POSITION_STATUS, 0x00, 0x00};
72 return set_cmd(f, CMD_PRIVATE, d, sizeof(d));
73}
74
75bool create_get_name(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power) {
76 init_frame(f, true, true, false, low_power);
77 set_dst(f, dst);
78 set_src(f, own);
79 return set_cmd(f, CMD_GET_NAME);
80}
81
82bool create_set_name(IoFrame &f, const uint8_t *own, const uint8_t *dst,
83 const uint8_t payload[DEVICE_NAME_WRITE_PAYLOAD_SIZE]) {
84 init_frame(f, true, true, false, false);
85 set_dst(f, dst);
86 set_src(f, own);
88}
89
90/// Build a tilt execute command (0x00) for devices that support slat angle control.
91bool create_execute_tilt(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t tilt_percent) {
92 init_frame(f, true, true, false, low_power);
93 set_dst(f, dst);
94 set_src(f, own);
95
96 auto const tilt_value =
97 static_cast<uint16_t>((POSITION_PERCENT_MAX - tilt_percent) * STATUS_POS_MAX / POSITION_PERCENT_MAX);
98 uint8_t d[EXECUTE_PAYLOAD_SIZE] = {EXECUTE_USER_ORIGINATOR,
99 EXECUTE_TILT_ACEI,
101 0x00,
103 static_cast<uint8_t>(tilt_value >> BITS_PER_BYTE),
104 static_cast<uint8_t>(tilt_value),
105 0x00};
106 return set_cmd(f, CMD_EXECUTE, d, sizeof(d));
107}
108
109/// Build a tilt-aware get-status request (0x03) that returns the extended 16-byte tilt payload.
110bool create_get_status_tilt(IoFrame &f, const uint8_t *own, const uint8_t *dst) {
111 init_frame(f, true, true, false, true);
112 set_dst(f, dst);
113 set_src(f, own);
114 // The selector byte switches the private status response to the extended tilt layout.
115 uint8_t d[4] = {PRIVATE_GET_POSITION_STATUS, STATUS_TILT_SELECTOR, 0x01, 0x00};
116 return set_cmd(f, CMD_PRIVATE, d, sizeof(d));
117}
118
119/// Build a discovery broadcast (0x28). Sent to the broadcast address 0x00003B.
120/// Only devices in pairing mode (PROG button pressed) will respond.
121bool create_discover(IoFrame &f, const uint8_t *own) {
122 // start+end: single broadcast frame.
123 init_frame(f, true, true, true, false);
125 set_src(f, own);
126 return set_cmd(f, CMD_DISCOVER_REQ);
127}
128
129/// Build a key-init request (0x31) to start the pairing key exchange with a discovered device.
130bool create_key_init(IoFrame &f, const uint8_t *own, const uint8_t *dst) {
131 init_frame(f, true, true, false, true);
132 set_dst(f, dst);
133 set_src(f, own);
134 return set_cmd(f, CMD_KEY_INIT);
135}
136
137/// Build a key-transfer frame (0x32) containing the system key encrypted with the transfer key.
138bool create_key_transfer(IoFrame &f, IoFrame &old_frame, const uint8_t *dst, const uint8_t *src,
139 const uint8_t key[AES_KEY_SIZE], const uint8_t challenge[HMAC_SIZE]) {
140 init_frame(f, true, false, false, false);
141 set_dst(f, dst);
142 set_src(f, src);
143 // The pairing capture we matched derives the IV from the previous command byte only. Treating
144 // the key-init frame that narrowly keeps our key transfer aligned with real controllers.
145 uint8_t enc_key[AES_KEY_SIZE];
146 if (!crypto::crypt_key(&old_frame.cmd, 1, challenge, key, enc_key))
147 return false;
148 return set_cmd(f, CMD_KEY_TRANSFER, enc_key, AES_KEY_SIZE);
149}
150
151/// Build a challenge request (0x3C) containing 6 random bytes.
152/// Used when WE need to authenticate an incoming request from a device.
153bool create_challenge_req(IoFrame &f, const uint8_t *dst, const uint8_t *src) {
154 init_frame(f, true, true, false, false); // start=true, end=false
155 set_dst(f, dst);
156 set_src(f, src);
157 uint8_t challenge[HMAC_SIZE];
159 return set_cmd(f, CMD_CHALLENGE_REQ, challenge, HMAC_SIZE);
160}
161
162/// Build a challenge response (0x3D) proving we know the system key.
163/// The HMAC is computed over [original_command_id + original_data] using the challenge.
164bool create_challenge_resp(IoFrame &f, const uint8_t *dst, const uint8_t *src, const uint8_t challenge[HMAC_SIZE],
165 const IoFrame &origin, const uint8_t *key) {
166 init_frame(f);
167 set_dst(f, dst);
168 set_src(f, src);
169 // The authenticated transcript covers the original request, not the 0x3D wrapper. Using the
170 // origin command byte and payload here was one of the key interoperability findings.
171 uint8_t frame_data[FRAME_MAX_SIZE];
172 frame_data[0] = origin.cmd;
173 memcpy(frame_data + 1, origin.data, origin.data_len);
174 uint8_t hmac[HMAC_SIZE];
175 if (!crypto::create_hmac(frame_data, origin.data_len + 1, challenge, key, hmac))
176 return false;
177 return set_cmd(f, CMD_CHALLENGE_RESP, hmac, HMAC_SIZE);
178}
179
180/// Build a status-update acknowledgment (0x72). Sent after authenticating a device's status update.
181/// The response is sent on all 3 channels to ensure the device receives it.
182bool create_status_update_resp(IoFrame &f, const uint8_t *own, const uint8_t *dst) {
183 // end=true: final frame.
184 init_frame(f, true, false, true, false);
185 set_dst(f, dst);
186 set_src(f, own);
187 // Status update acknowledgment payload matched from working controller captures.
188 return set_cmd(f, CMD_STATUS_UPDATE_RESP, STATUS_UPDATE_ACK_PAYLOAD, sizeof(STATUS_UPDATE_ACK_PAYLOAD));
189}
190
191/// Build a set-config command (0x6F) to tell the device to automatically send status updates
192/// when controlled by any remote (not just us). Not all devices support this.
193bool create_set_config1(IoFrame &f, const uint8_t *own, const uint8_t *dst) {
194 init_frame(f, true, true, false, false);
195 set_dst(f, dst);
196 set_src(f, own);
197 // Set-config payload matched from working controller captures.
198 return set_cmd(f, CMD_SET_CONFIG1, SET_CONFIG1_STATUS_BROADCAST_PAYLOAD,
199 sizeof(SET_CONFIG1_STATUS_BROADCAST_PAYLOAD));
200}
201
202} // namespace home_io_control
203} // 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 set_cmd(IoFrame &f, uint8_t cmd, const uint8_t *params, uint8_t params_len)
Set command and payload.
bool create_get_name(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build a get-name request (0x50).
static constexpr uint8_t BITS_PER_BYTE
Number of bits in one protocol byte.
Definition proto_frame.h:88
static constexpr uint8_t CMD_DISCOVER_REQ
Broadcast discovery request.
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.
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 POS_UNKNOWN
Position unknown.
bool create_execute(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t position)
Build an execute command (0x00) to control a device.
static constexpr uint8_t CMD_GET_NAME
Request device name.
static constexpr uint8_t HMAC_SIZE
Authentication HMAC is 6 bytes (truncated AES output).
Definition proto_frame.h:83
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 FRAME_MAX_SIZE
Maximum frame size (9 header + 23 data).
Definition proto_frame.h:91
static constexpr uint8_t DEVICE_NAME_WRITE_PAYLOAD_SIZE
Fixed write payload: 15 visible chars plus trailing null/padding.
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.
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 CMD_EXECUTE
Set position/open/close/stop — requires authentication.
static constexpr uint8_t CMD_CHALLENGE_REQ
Device sends 6-byte random challenge.
static constexpr uint8_t CMD_STATUS_UPDATE_RESP
Acknowledge status update.
static constexpr uint8_t CMD_SET_NAME
Set device name (authenticated).
bool create_challenge_req(IoFrame &f, const uint8_t *dst, const uint8_t *src)
Build a challenge request (0x3C) containing 6 random bytes.
bool create_discover(IoFrame &f, const uint8_t *own)
Build a discovery broadcast (0x28).
static constexpr uint8_t POS_FAVORITE
Move to favorite/"My" position.
static constexpr uint8_t BROADCAST_DISCOVER[NODE_ID_SIZE]
Broadcast address for device discovery (0x00003B).
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_PRIVATE
Get device status — no authentication needed.
static constexpr uint8_t AES_KEY_SIZE
AES-128 key size.
Definition proto_frame.h:84
static constexpr uint8_t CMD_CHALLENGE_RESP
Controller responds with HMAC proof.
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_status_update_resp(IoFrame &f, const uint8_t *own, const uint8_t *dst)
Build a status-update acknowledgment (0x72).
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.
Cryptographic helpers for the IO‑Homecontrol protocol.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
uint8_t data[FRAME_MAX_DATA_SIZE]
Command parameters (0–23 bytes).
uint8_t data_len
Actual length of data.