Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
proto_frame.h
Go to the documentation of this file.
1#pragma once
2
3/// @file proto_frame.h
4/// @brief IO-Homecontrol 2W protocol definitions.
5///
6/// IO-Homecontrol is a proprietary wireless protocol used by Somfy, Velux, and other
7/// manufacturers for controlling shutters, awnings, blinds, and similar devices.
8/// "2W" means two-way: the controller sends commands and receives status feedback.
9///
10/// The protocol uses FSK modulation at 868 MHz with frequency hopping across 3 channels.
11/// Communication is encrypted with AES-128 and authenticated with a 6-byte HMAC.
12/// Each installation has a unique 16-byte "system key" shared between controller and devices.
13
14#include <cstdint>
15#include <cstring>
16#include <string>
17
18namespace esphome {
19namespace home_io_control {
20
21// ============================================================================
22// Physical Layer — Radio Parameters
23// ============================================================================
24
25/// The protocol uses 3 frequency channels in the 868 MHz ISM band.
26/// IO-Homecontrol uses 3 channels in the 868 MHz SRD band. In 1W (one-way) mode,
27/// only CH2 is used. In 2W (two-way) mode, the controller hops across all three
28/// channels every ~2.7ms when idle. Commands are sent on CH2; responses may arrive
29/// on any channel within the exchange wait window.
30static constexpr uint32_t FREQ_CH1 = 868250000; ///< Channel 1: 868.25 MHz (2W only)
31static constexpr uint32_t FREQ_CH2 = 868950000; ///< Channel 2: 868.95 MHz (1W and 2W, TX channel)
32static constexpr uint32_t FREQ_CH3 = 869850000; ///< Channel 3: 869.85 MHz (2W only)
33
34/// Preamble is a sequence of 0xAA bytes that precedes every frame.
35/// The first frame in an exchange uses a long preamble (1024 bytes = 8192 bits)
36/// so the receiver has time to detect it while hopping. Subsequent frames in the
37/// same exchange use a short preamble (8 bytes) since both sides are already
38/// on the same channel. Solar-powered devices need the long preamble to wake up.
39static constexpr uint16_t LONG_PREAMBLE = 1024; ///< 1024 bytes for initial/start frames
40static constexpr uint16_t SHORT_PREAMBLE = 8; ///< 8 bytes for response/continuation frames
41/// SX1262-specific preamble for the outbound 0x3D challenge response.
42///
43/// The SX1262 path has to rebuild the IO-homecontrol framing details in software and the
44/// 0x3D auth response is sent immediately after we switch from RX to TX upon receiving the
45/// device's 0x3C challenge. Real-device tuning showed that 64 preamble bytes gives the peer
46/// enough lock-on margin in that tight turn-around window, while leaving the proven SX1276
47/// short-response waveform untouched.
48static constexpr uint16_t SX1262_AUTH_RESPONSE_PREAMBLE = 64;
49
50/// SX1262-specific per-channel dwell while waiting for authenticated exchange responses.
51///
52/// A 50 ms dwell was short enough that the controller could hop away from the request channel
53/// just before the device emitted its post-auth reply. Using 90 ms keeps the SX1262 receiver on
54/// that channel long enough for the observed device turn-around after 0x3D, without inflating the
55/// overall 300/500 ms exchange windows for the rest of the protocol.
56static constexpr int32_t SX1262_EXCHANGE_RESPONSE_WAIT_SLICE_MS = 90;
57
58/// Timing constants for frequency hopping and response waiting.
59static constexpr int32_t HOP_TIME_US = 2700; ///< Time per channel when hopping (2.7ms)
60static constexpr int32_t RESPONSE_CHANNEL_WAIT_MS = 50; ///< Per-channel dwell while waiting for an exchange response
61static constexpr int32_t RESPONSE_WAIT_MS = 500; ///< Wait for response to non-start frame
62static constexpr int32_t RESPONSE_START_WAIT_MS = 300; ///< Wait for response to start frame (longer)
63static constexpr int32_t RESPONSE_AUTH_WAIT_MS =
64 RESPONSE_WAIT_MS; ///< Wait for final response after challenge response
65static constexpr int32_t EXCHANGE_RETRY_DELAY_MS = 250; ///< Gap between retries within one HA command
66static constexpr uint8_t EXCHANGE_RETRY_COUNT = 3; ///< Attempts per command before reporting failure
67
68/// Listen-before-talk (LBT) parameters for ETSI EN 300 220 compliance.
69/// Before transmitting, the radio checks that the channel RSSI is below the
70/// threshold. If the channel is busy, TX is deferred by LBT_RETRY_DELAY_MS
71/// up to LBT_MAX_RETRIES times.
72static constexpr int16_t LBT_RSSI_THRESHOLD_DBM = -90; ///< Channel-free threshold (ETSI: ≤ -90 dBm)
73static constexpr uint8_t LBT_MAX_RETRIES = 5; ///< Max carrier-sense attempts before TX anyway
74static constexpr uint8_t LBT_RETRY_DELAY_MS = 5; ///< Backoff between LBT checks (≥ 5ms per ETSI)
75
76// ============================================================================
77// Frame Constants
78// ============================================================================
79
80static constexpr uint8_t NODE_ID_SIZE = 3; ///< Device/node addresses are 3 bytes (e.g., "123ABC")
81static constexpr uint8_t NODE_ID_STRING_SIZE = NODE_ID_SIZE * 2 + 1; ///< Uppercase hex node ID plus null terminator
82static constexpr uint8_t HMAC_SIZE = 6; ///< Authentication HMAC is 6 bytes (truncated AES output)
83static constexpr uint8_t AES_KEY_SIZE = 16; ///< AES-128 key size
84static constexpr uint8_t AES_BLOCK_SIZE = 16; ///< AES block size
85static constexpr uint8_t IV_SIZE = 16; ///< Initialization vector size for AES
86static constexpr uint8_t IV_PADDING = 0x55; ///< Padding byte used in IV construction
87static constexpr uint8_t BITS_PER_BYTE = 8; ///< Number of bits in one protocol byte
88
89static constexpr uint8_t FRAME_MIN_SIZE = 9; ///< Minimum frame: CTRL0+CTRL1+DST(3)+SRC(3)+CMD(1)
90static constexpr uint8_t FRAME_MAX_SIZE = 32; ///< Maximum frame size (9 header + 23 data)
91static constexpr uint8_t FRAME_MAX_DATA_SIZE = 23; ///< Maximum data bytes after command ID
92
93/// Control byte 0 (CTRL0) bit definitions.
94/// CTRL0 encodes frame flags and the total frame length.
95/// Bits [4:0] = frame_length - 1 (so 0x08 means 9 bytes total).
96/// - START (bit 6): first frame in an exchange; uses long preamble (1024 bytes).
97/// - END (bit 7): last frame in an exchange; set on responses and command completions.
98/// - 1W (bit 5): 1=OneWay protocol (no response expected), 0=TwoWay (response expected).
99/// For 2W operation, the controller sets START on initial command and device replies with END; subsequent frames in an
100/// authenticated exchange also carry END.
101static constexpr uint8_t CTRL0_END = 0x80; ///< Bit 7: last frame in exchange
102static constexpr uint8_t CTRL0_START = 0x40; ///< Bit 6: first frame in exchange (uses long preamble)
103static constexpr uint8_t CTRL0_PROTOCOL_1W = 0x20; ///< Bit 5: 1=OneWay protocol, 0=TwoWay protocol
104static constexpr uint8_t CTRL0_LENGTH_MASK = 0x1F; ///< Bits [4:0]: frame length - 1
105
106/// Control byte 1 (CTRL1) bit definitions.
107/// - LOW_POWER (bit 5): device is battery/solar powered; may sleep and requires long preamble to wake.
108static constexpr uint8_t CTRL1_LOW_POWER = 0x20; ///< Bit 5: low-power device (e.g., solar-powered)
109
110// ============================================================================
111// Command IDs
112// ============================================================================
113
114// Normal operation commands
115static constexpr uint8_t CMD_EXECUTE = 0x00; ///< Set position/open/close/stop — requires authentication
116static constexpr uint8_t CMD_PRIVATE = 0x03; ///< Get device status — no authentication needed
117static constexpr uint8_t CMD_PRIVATE_RESP = 0x04; ///< Response to 0x00 and 0x03 (contains position data)
118
119// Discovery and pairing commands
120static constexpr uint8_t CMD_DISCOVER_REQ = 0x28; ///< Broadcast discovery request
121static constexpr uint8_t CMD_DISCOVER_RESP = 0x29; ///< Device responds with its ID and type
122static constexpr uint8_t CMD_DISCOVER_SPE_REQ = 0x2A; ///< Discover sub-devices (e.g., light on garage door)
123static constexpr uint8_t CMD_DISCOVER_SPE_RESP = 0x2B; ///< Sub-device response
124static constexpr uint8_t CMD_DISCOVER_CONFIRM = 0x2C; ///< Confirm discovery to device
125static constexpr uint8_t CMD_DISCOVER_CONFIRM_ACK = 0x2D; ///< Device acknowledges confirmation
126
127// Key exchange commands (used during pairing)
128static constexpr uint8_t CMD_KEY_INIT = 0x31; ///< Initiate key transfer to device
129static constexpr uint8_t CMD_KEY_TRANSFER = 0x32; ///< Send encrypted system key to device
130static constexpr uint8_t CMD_KEY_CONFIRM = 0x33; ///< Device confirms key was received
131
132// Authentication commands (challenge-response for secured commands)
133static constexpr uint8_t CMD_CHALLENGE_REQ = 0x3C; ///< Device sends 6-byte random challenge
134static constexpr uint8_t CMD_CHALLENGE_RESP = 0x3D; ///< Controller responds with HMAC proof
135
136// Device info commands
137static constexpr uint8_t CMD_GET_NAME = 0x50; ///< Request device name
138static constexpr uint8_t CMD_GET_NAME_RESP = 0x51; ///< Device name response
139static constexpr uint8_t CMD_GET_INFO2 = 0x56; ///< Request device type/model info
140static constexpr uint8_t CMD_GET_INFO2_RESP = 0x57; ///< Device type/model response
141
142// Configuration and status update commands
143static constexpr uint8_t CMD_SET_CONFIG1 = 0x6F; ///< Configure device to auto-send status updates
144static constexpr uint8_t CMD_SET_CONFIG1_RESP = 0x70; ///< Config response
145static constexpr uint8_t CMD_STATUS_UPDATE = 0x71; ///< Device-initiated status update (needs auth)
146static constexpr uint8_t CMD_STATUS_UPDATE_RESP = 0x72; ///< Acknowledge status update
147static constexpr uint8_t CMD_ERROR_RESP = 0xFE; ///< Error response to any command
148
149// ============================================================================
150// Position and Status Constants
151// ============================================================================
152
153/// Position values in the IO protocol.
154/// Normal positions are 0-100 (0=fully open, 100=fully closed).
155/// Special values above 100 are control commands.
156static constexpr uint8_t POS_STOP = 0xD2; ///< Stop movement
157static constexpr uint8_t POS_UNKNOWN = 0xD4; ///< Position unknown
158static constexpr uint8_t POS_FAVORITE = 0xD8; ///< Move to favorite/"My" position
159
160/// In status responses, position is encoded as a 16-bit value where
161/// 0x0000 = fully open (0%) and 0xC800 = fully closed (100%).
162static constexpr uint16_t STATUS_POS_MAX = 0xC800;
163/// Target-reached tolerance expressed in raw IO-homecontrol position units.
164/// 100 raw units out of 51200 full-scale is about 0.195%, so this only absorbs
165/// tiny target/current mismatches from device rounding or early stopped flags.
166static constexpr uint16_t STATUS_POS_TOLERANCE_RAW = 100;
167
168/// Status byte flags in CMD_PRIVATE_RESP and CMD_STATUS_UPDATE.
169static constexpr uint8_t STATUS_STOPPED = 0x01; ///< Byte 0 bit 0: device is not moving
170static constexpr uint8_t STATUS_EXPECTED = 0x80; ///< Byte 1 bit 7: device will send auto status update
171static constexpr uint8_t STATUS_TILT_SELECTOR = 0x20; ///< Extended status payload marker for tilt-capable devices
172
173/// Packed device metadata uses two bytes where the high 8 bits carry the upper type bits and the
174/// low byte carries both the remaining type bits and the 6-bit manufacturer subtype.
175static constexpr uint8_t DEVICE_METADATA_SIZE = 2;
176static constexpr uint8_t DEVICE_TYPE_LOW_BITS_SHIFT = 2;
177static constexpr uint8_t DEVICE_TYPE_HIGH_BITS_SHIFT = 6;
178static constexpr uint8_t DEVICE_SUBTYPE_MASK = 0x3F;
179
180// ============================================================================
181// Cryptographic Constants
182// ============================================================================
183
184/// The transfer key is a hardcoded key used ONLY during pairing to obfuscate
185/// the system key during over-the-air transfer. It is NOT the system key.
186/// This is the same across all IO-Homecontrol devices worldwide.
187static constexpr uint8_t TRANSFER_KEY[AES_KEY_SIZE] = {0x34, 0xC3, 0x46, 0x6E, 0xD8, 0x8F, 0x4E, 0x8E,
188 0x16, 0xAA, 0x47, 0x39, 0x49, 0x88, 0x43, 0x73};
189static constexpr uint16_t CRC_POLYNOMIAL_REVERSED = 0x8408; ///< Reversed CRC-CCITT polynomial used by IO-homecontrol
190static constexpr uint16_t CRC_LSB_MASK = 0x0001; ///< Least-significant-bit mask for reflected CRC update
191
192/// Broadcast address for device discovery (0x00003B).
193/// Used as destination in CMD_DISCOVER_REQ frames to trigger all pairable devices to respond.
194static constexpr uint8_t BROADCAST_DISCOVER[NODE_ID_SIZE] = {0x00, 0x00, 0x3B};
195
196// ============================================================================
197// Frame Structure
198// ============================================================================
199
200/// @brief Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
201///
202/// Over the air layout: [CTRL0][CTRL1][DST 3B][SRC 3B][CMD][DATA 0-23B][CRC 2B].
203/// The CRC is handled by hardware on SX1276 (IoHomeOn) and by software on SX1262;
204/// it is not included in this struct.
205struct IoFrame {
206 uint8_t ctrl0; ///< Control byte 0: flags + length.
207 uint8_t ctrl1; ///< Control byte 1: low power, beacon, etc.
208 uint8_t dst[NODE_ID_SIZE]; ///< Destination node ID (3 bytes).
209 uint8_t src[NODE_ID_SIZE]; ///< Source node ID (3 bytes).
210 uint8_t cmd; ///< Command ID.
211 uint8_t data[FRAME_MAX_DATA_SIZE]; ///< Command parameters (0–23 bytes).
212 uint8_t data_len; ///< Actual length of data.
213};
214
215// --- Frame construction and parsing ---
216/// Initialize an IoFrame header (ctrl0/ctrl1) with flags.
217/// @param f Frame to initialize.
218/// @param is_2w True for 2‑way (default), false for 1‑way.
219/// @param start Set START flag (first frame in exchange).
220/// @param end Set END flag (final frame in exchange).
221/// @param low_power Set LOW_POWER flag.
222void init_frame(IoFrame &f, bool is_2w = true, bool start = false, bool end = false, bool low_power = false);
223/// Set destination node ID.
224/// @param f Frame to modify.
225/// @param id 3‑byte destination address.
226void set_dst(IoFrame &f, const uint8_t id[NODE_ID_SIZE]);
227/// Set source node ID.
228/// @param f Frame to modify.
229/// @param id 3‑byte source address.
230void set_src(IoFrame &f, const uint8_t id[NODE_ID_SIZE]);
231/// Set command and payload.
232/// @param f Frame to modify.
233/// @param cmd Command ID.
234/// @param params Pointer to payload bytes (may be nullptr for zero‑length).
235/// @param params_len Payload length (0–23).
236/// @return true if frame fits within size limits; false otherwise.
237bool set_cmd(IoFrame &f, uint8_t cmd, const uint8_t *params = nullptr, uint8_t params_len = 0);
238/// Get total frame length from ctrl0.
239/// @param f Parsed frame.
240/// @return Length in bytes.
241uint8_t frame_length(const IoFrame &f);
242/// Check START flag.
243/// @param f Parsed frame.
244/// @return true if START flag is set.
245bool is_start(const IoFrame &f);
246/// Check END flag.
247/// @param f Parsed frame.
248/// @return true if END flag is set.
249bool is_end(const IoFrame &f);
250/// Serialize a parsed frame into a wire buffer (without CRC).
251/// @param f Parsed frame.
252/// @param buf Output buffer (must be at least frame_length(f) bytes).
253/// @param buf_size Size of buf.
254/// @return Number of bytes written, or 0 on failure.
255uint8_t serialize(const IoFrame &f, uint8_t *buf, uint8_t buf_size);
256/// Parse a wire buffer into a parsed IoFrame (validates length and CTRL0).
257/// @param buf Raw byte buffer.
258/// @param buf_len Number of bytes in buf.
259/// @param f Output parsed frame.
260/// @return true if parse succeeded; false otherwise.
261bool parse(const uint8_t *buf, uint8_t buf_len, IoFrame &f);
262
263// ============================================================================
264// Device Types — from the IO-Homecontrol specification
265// ============================================================================
266
267/// @brief Device type identifiers reported by IO‑Homecontrol products.
268/// The numeric values follow the official specification. Do not reassign or reorder these.
269enum class DeviceType : uint8_t {
270 UNKNOWN = 0x00, ///< Unknown/unspecified device.
271 VENETIAN_BLIND = 0x01, ///< Venetian blind.
272 ROLLER_SHUTTER = 0x02, ///< Roller shutter.
273 AWNING = 0x03, ///< Awning.
274 WINDOW_OPENER = 0x04, ///< Window opening actuator.
275 GARAGE_OPENER = 0x05, ///< Garage door opener.
276 LIGHT = 0x06, ///< Binary light.
277 GATE_OPENER = 0x07, ///< Gate opener.
278 ROLLING_DOOR_OPENER = 0x08, ///< Rolling door opener.
279 LOCK = 0x09, ///< Lock.
280 BLIND = 0x0A, ///< Generic blind.
281 SCREEN = 0x0B, ///< Insect/privacy screen.
282 BEACON = 0x0C, ///< Beacon (unpaired/announcement).
283 DUAL_SHUTTER = 0x0D, ///< Dual-section shutter.
284 HEATING_TEMPERATURE_INTERFACE = 0x0E, ///< Heating temperature interface.
285 ON_OFF_SWITCH = 0x0F, ///< Generic on/off switch.
286 HORIZONTAL_AWNING = 0x10, ///< Horizontal awning (open/close inverted).
287 EXTERNAL_VENETIAN_BLIND = 0x11, ///< External venetian blind.
288 LOUVRE_BLIND = 0x12, ///< Louvre blind.
289 CURTAIN_TRACK = 0x13, ///< Curtain track.
290 VENTILATION_POINT = 0x14, ///< Ventilation point.
291 EXTERIOR_HEATING = 0x15, ///< Exterior heating.
292 HEAT_PUMP = 0x16, ///< Heat pump.
293 INTRUSION_ALARM = 0x17, ///< Intrusion alarm.
294 SWINGING_SHUTTER = 0x18, ///< Swinging shutter.
295};
296
297/// @brief High‑level capability class derived from DeviceType.
298enum class DeviceCapabilityClass : uint8_t {
299 UNKNOWN = 0x00, ///< Unknown capability.
300 COVER = 0x01, ///< Position‑controlled cover (shutter/blind/awning).
301 LIGHT = 0x02, ///< Binary on/off light.
302 SWITCH = 0x03, ///< Binary on/off switch.
303 SENSOR = 0x04, ///< Sensor device.
304 BEACON = 0x05, ///< Beacon.
305 CLIMATE = 0x06, ///< Climate device (heating/cooling).
306 LOCK = 0x07, ///< Lock.
307};
308
309/// @brief Convert a DeviceType to a lowercase string identifier.
310/// @param type Device type enum.
311/// @return Null‑terminated string name (e.g., "roller_shutter").
312const char *device_type_name(DeviceType type);
313
314/// @brief Map a raw IO‑Homecontrol type to the closest ESPHome/Home Assistant entity family.
315/// @param type Raw device type.
316/// @return Capability class (COVER, LIGHT, SWITCH, etc.).
318
319/// @brief Get a human‑readable name for a capability class.
320/// @param type Device type (unused, kept for signature compatibility).
321/// @return String like "cover", "light", "switch", "unknown".
323
324/// @brief Does this device type support precise position control (0–100)?
325/// @param type Device type.
326/// @return true for cover‑family devices.
328
329/// @brief Does this device type support binary on/off control?
330/// @param type Device type.
331/// @return true for lights and switches.
333
334/// @brief Does this device type support status request commands (0x03)?
335/// @param type Device type.
336/// @return true for covers and binary devices.
338
339/// @brief Does this device type support tilt (slat angle) control?
340/// @param type Device type.
341/// @return true for venetian blinds, blinds, external venetian blinds, louvre blinds.
343
344/// @brief Decode a protocol-packed device type from two metadata bytes.
345/// @param type_msb First metadata byte.
346/// @param type_subtype Second metadata byte containing the remaining type bits and subtype.
347/// @return Decoded device type.
348DeviceType decode_packed_device_type(uint8_t type_msb, uint8_t type_subtype);
349
350/// @brief Decode a protocol-packed device subtype from the second metadata byte.
351/// @param type_subtype Second metadata byte containing subtype in bits [5:0].
352/// @return Manufacturer-specific subtype.
353uint8_t decode_packed_device_subtype(uint8_t type_subtype);
354
355/// @brief Human‑readable operation profile name for a device type.
356/// Used for logging and diagnostics.
357/// @param type Device type.
358/// @return String such as "cover_position", "cover_position_tilt", "binary_on_off", "lock", etc.
360
361// ============================================================================
362// Device State
363// ============================================================================
364
365/// @brief Sentinel value meaning "position is not known yet".
366/// Matches POS_UNKNOWN (0xD4 = 212 decimal) for easy debugging.
367static constexpr float UNKNOWN_POSITION = 212.0F;
368static constexpr uint8_t DEVICE_NAME_BUFFER_SIZE = 32; ///< Device name storage including null terminator
369
370/// @brief Runtime state of a paired IO‑Homecontrol device.
371struct IoDevice {
372 uint8_t node_id[NODE_ID_SIZE]{}; ///< Device's 3‑byte radio address.
373 DeviceType type{DeviceType::UNKNOWN}; ///< Device type (shutter, awning, etc.).
374 uint8_t subtype{0}; ///< Device subtype (manufacturer‑specific).
375 char name[DEVICE_NAME_BUFFER_SIZE]{}; ///< Device name (from device, Latin‑1 encoded).
376 float position{UNKNOWN_POSITION}; ///< Current position: 0=open, 100=closed, or UNKNOWN_POSITION.
377 float tilt{UNKNOWN_POSITION}; ///< Current tilt: 0=closed, 100=open, or UNKNOWN_POSITION.
378 float target{UNKNOWN_POSITION}; ///< Target position the device is moving toward.
379 bool is_stopped{true}; ///< True if device is not moving.
380 bool inverted{false}; ///< True if open/close positions are swapped (e.g., horizontal awning).
381 bool single_follow_up_poll_pending{false}; ///< True when one legacy settle poll should still be scheduled.
382 uint32_t last_status{0}; ///< millis() timestamp of last received status.
383 uint32_t status_poll_interval_ms{0}; ///< Configured follow-up poll interval while a state change is expected.
384 uint32_t next_update{0}; ///< millis() timestamp when we should poll for status next.
385 uint32_t poll_deadline{0}; ///< Hard stop for bounded follow-up polling after a command or remote activity.
386 uint8_t status_poll_failures{0}; ///< Consecutive background status-poll failures without a valid reply.
387 uint8_t auth_poll_failures{0}; ///< Consecutive background poll failures that reached 0x3C auth challenge.
388};
389
390/// @brief Convert a hex string (e.g., "123ABC") to a byte array.
391/// @param hex Hex string (must be exactly len*2 characters).
392/// @param out Output buffer (at least len bytes).
393/// @param len Number of bytes to produce.
394/// @return true on success; false if hex length mismatch or non‑hex characters.
395bool hex_to_bytes(const std::string &hex, uint8_t *out, uint8_t len);
396/// @brief Format a 3‑byte node ID as a 6‑character uppercase hex string.
397/// @param id 3‑byte node ID.
398/// @return Hex string (e.g., "123ABC").
399std::string node_id_to_string(const uint8_t id[NODE_ID_SIZE]);
400/// @brief Determine whether a device type has inverted position mapping by default.
401/// @param type Device type.
402/// @return true for horizontal awnings; false otherwise.
404/// @brief Decode target/current position values from a status frame.
405/// @param target_raw 16‑bit raw target value.
406/// @param current_raw 16‑bit raw current value.
407/// @param is_stopped True if device reports stopped.
408/// @param target Output target position (0–100 or UNKNOWN_POSITION).
409/// @param position Output current position (0–100 or UNKNOWN_POSITION).
410void decode_position_report(uint16_t target_raw, uint16_t current_raw, bool is_stopped, float &target, float &position);
411/// @brief Has the device reached its target within tolerance?
412/// @param target Target position (0–100 or UNKNOWN_POSITION).
413/// @param position Current position (0–100 or UNKNOWN_POSITION).
414/// @return true if positions match within STATUS_POS_TOLERANCE_RAW.
415bool has_reached_target_position(float target, float position);
416/// @brief Decode tilt angle from raw 16‑bit value.
417/// @param tilt_raw Raw tilt value from status frame.
418/// @return Tilt percentage (0 = closed, 100 = open) or UNKNOWN_POSITION.
419float decode_tilt_report(uint16_t tilt_raw);
420/// @brief Compute CRC‑CCITT (poly 0x1021, init 0x0000) over a buffer.
421/// On SX1276 this is done in hardware; on SX1262 it is computed in software.
422/// @param data Pointer to data bytes.
423/// @param len Number of bytes.
424/// @return 16‑bit CRC value.
425uint16_t crc_ccitt(const uint8_t *data, uint8_t len);
426
427} // namespace home_io_control
428} // namespace esphome
bool set_cmd(IoFrame &f, uint8_t cmd, const uint8_t *params, uint8_t params_len)
Set command and payload.
static constexpr uint8_t DEVICE_NAME_BUFFER_SIZE
Device name storage including null terminator.
const char * device_operation_profile_name(DeviceType type)
Human‑readable operation profile name for a device type.
static constexpr uint8_t BITS_PER_BYTE
Number of bits in one protocol byte.
Definition proto_frame.h:87
static constexpr uint8_t DEVICE_METADATA_SIZE
Packed device metadata uses two bytes where the high 8 bits carry the upper type bits and the low byt...
static constexpr float UNKNOWN_POSITION
Sentinel value meaning "position is not known yet".
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_frame.h:80
static constexpr uint8_t AES_BLOCK_SIZE
AES block size.
Definition proto_frame.h:84
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 FRAME_MIN_SIZE
Minimum frame: CTRL0+CTRL1+DST(3)+SRC(3)+CMD(1).
Definition proto_frame.h:89
static constexpr uint8_t TRANSFER_KEY[AES_KEY_SIZE]
The transfer key is a hardcoded key used ONLY during pairing to obfuscate the system key during over-...
static constexpr uint8_t CMD_ERROR_RESP
Error response to any command.
static constexpr uint8_t CMD_GET_NAME_RESP
Device name response.
static constexpr uint8_t CMD_DISCOVER_CONFIRM_ACK
Device acknowledges confirmation.
static constexpr uint8_t DEVICE_TYPE_HIGH_BITS_SHIFT
DeviceType
Device type identifiers reported by IO‑Homecontrol products.
@ DUAL_SHUTTER
Dual-section shutter.
@ HEATING_TEMPERATURE_INTERFACE
Heating temperature interface.
@ BEACON
Beacon (unpaired/announcement).
@ EXTERNAL_VENETIAN_BLIND
External venetian blind.
@ UNKNOWN
Unknown/unspecified device.
@ WINDOW_OPENER
Window opening actuator.
@ HORIZONTAL_AWNING
Horizontal awning (open/close inverted).
@ ROLLING_DOOR_OPENER
Rolling door opener.
@ ON_OFF_SWITCH
Generic on/off switch.
@ SCREEN
Insect/privacy screen.
@ VENTILATION_POINT
Ventilation point.
static constexpr uint8_t CTRL0_END
Control byte 0 (CTRL0) bit definitions.
static constexpr uint8_t FRAME_MAX_DATA_SIZE
Maximum data bytes after command ID.
Definition proto_frame.h:91
static constexpr uint8_t POS_UNKNOWN
Position unknown.
static constexpr uint16_t STATUS_POS_TOLERANCE_RAW
Target-reached tolerance expressed in raw IO-homecontrol position units.
DeviceCapabilityClass device_capability_class(DeviceType type)
Map a raw IO‑Homecontrol type to the closest ESPHome/Home Assistant entity family.
static constexpr uint8_t CMD_STATUS_UPDATE
Device-initiated status update (needs auth).
bool default_inverted_for_type(DeviceType type)
Determine whether a device type has inverted position mapping by default.
static constexpr uint8_t LBT_MAX_RETRIES
Max carrier-sense attempts before TX anyway.
Definition proto_frame.h:73
bool is_start(const IoFrame &f)
Check START flag.
static constexpr uint32_t FREQ_CH1
The protocol uses 3 frequency channels in the 868 MHz ISM band.
Definition proto_frame.h:30
bool device_supports_position_control(DeviceType type)
Does this device type support precise position control (0–100)?
static constexpr uint32_t FREQ_CH3
Channel 3: 869.85 MHz (2W only).
Definition proto_frame.h:32
static constexpr uint8_t CMD_GET_NAME
Request device name.
static constexpr int32_t HOP_TIME_US
Timing constants for frequency hopping and response waiting.
Definition proto_frame.h:59
static constexpr uint8_t CTRL0_PROTOCOL_1W
Bit 5: 1=OneWay protocol, 0=TwoWay protocol.
const char * device_type_name(DeviceType type)
Convert a DeviceType to a lowercase string identifier.
static constexpr uint8_t CTRL0_START
Bit 6: first frame in exchange (uses long preamble).
bool device_supports_binary_control(DeviceType type)
Does this device type support binary on/off control?
static constexpr uint8_t CMD_DISCOVER_SPE_RESP
Sub-device response.
uint16_t crc_ccitt(const uint8_t *data, uint8_t len)
CRC-CCITT used by the IO-Homecontrol protocol for frame validation.
static constexpr uint8_t HMAC_SIZE
Authentication HMAC is 6 bytes (truncated AES output).
Definition proto_frame.h:82
void init_frame(IoFrame &f, bool is_2w, bool start, bool end, bool low_power)
Initialize an IoFrame header (ctrl0/ctrl1) with flags.
float decode_tilt_report(uint16_t tilt_raw)
Decode tilt angle from raw 16‑bit value.
uint8_t frame_length(const IoFrame &f)
Get total frame length from ctrl0.
static constexpr uint8_t FRAME_MAX_SIZE
Maximum frame size (9 header + 23 data).
Definition proto_frame.h:90
static constexpr uint8_t CMD_KEY_CONFIRM
Device confirms key was received.
static constexpr int32_t EXCHANGE_RETRY_DELAY_MS
Gap between retries within one HA command.
Definition proto_frame.h:65
static constexpr uint8_t CMD_KEY_INIT
Initiate key transfer to device.
void set_dst(IoFrame &f, const uint8_t id[NODE_ID_SIZE])
Set destination node ID.
DeviceType decode_packed_device_type(uint8_t type_msb, uint8_t type_subtype)
Decode a protocol-packed device type from two metadata bytes.
static constexpr uint8_t NODE_ID_STRING_SIZE
Uppercase hex node ID plus null terminator.
Definition proto_frame.h:81
bool is_end(const IoFrame &f)
Check END flag.
static constexpr uint16_t CRC_LSB_MASK
Least-significant-bit mask for reflected CRC update.
static constexpr int32_t SX1262_EXCHANGE_RESPONSE_WAIT_SLICE_MS
SX1262-specific per-channel dwell while waiting for authenticated exchange responses.
Definition proto_frame.h:56
bool parse(const uint8_t *buf, uint8_t buf_len, IoFrame &f)
Parse a wire buffer into a parsed IoFrame (validates length and CTRL0).
static constexpr uint8_t CMD_DISCOVER_SPE_REQ
Discover sub-devices (e.g., light on garage door).
static constexpr uint8_t EXCHANGE_RETRY_COUNT
Attempts per command before reporting failure.
Definition proto_frame.h:66
static constexpr uint8_t CMD_EXECUTE
Set position/open/close/stop — requires authentication.
static constexpr uint8_t CMD_PRIVATE_RESP
Response to 0x00 and 0x03 (contains position data).
const char * device_capability_class_name(DeviceType type)
Get a human‑readable name for a capability class.
static constexpr uint32_t FREQ_CH2
Channel 2: 868.95 MHz (1W and 2W, TX channel).
Definition proto_frame.h:31
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_CONFIG1_RESP
Config response.
std::string node_id_to_string(const uint8_t id[NODE_ID_SIZE])
Format a 3‑byte node ID as a 6‑character uppercase hex string.
static constexpr uint8_t CMD_DISCOVER_CONFIRM
Confirm discovery to device.
static constexpr uint8_t POS_FAVORITE
Move to favorite/"My" position.
static constexpr uint8_t STATUS_EXPECTED
Byte 1 bit 7: device will send auto status update.
uint8_t decode_packed_device_subtype(uint8_t type_subtype)
Decode a protocol-packed device subtype from the second metadata byte.
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 STATUS_TILT_SELECTOR
Extended status payload marker for tilt-capable devices.
bool device_supports_status_requests(DeviceType type)
Does this device type support status request commands (0x03)?
static constexpr uint8_t LBT_RETRY_DELAY_MS
Backoff between LBT checks (≥ 5ms per ETSI).
Definition proto_frame.h:74
static constexpr uint16_t SHORT_PREAMBLE
8 bytes for response/continuation frames
Definition proto_frame.h:40
static constexpr uint8_t CMD_PRIVATE
Get device status — no authentication needed.
static constexpr uint8_t DEVICE_SUBTYPE_MASK
static constexpr uint8_t CTRL0_LENGTH_MASK
Bits [4:0]: frame length - 1.
bool has_reached_target_position(float target, float position)
Has the device reached its target within tolerance?
static constexpr uint8_t AES_KEY_SIZE
AES-128 key size.
Definition proto_frame.h:83
DeviceCapabilityClass
High‑level capability class derived from DeviceType.
@ CLIMATE
Climate device (heating/cooling).
@ COVER
Position‑controlled cover (shutter/blind/awning).
static constexpr int32_t RESPONSE_WAIT_MS
Wait for response to non-start frame.
Definition proto_frame.h:61
static constexpr uint8_t CMD_GET_INFO2_RESP
Device type/model response.
static constexpr uint8_t POS_STOP
Position values in the IO protocol.
static constexpr uint8_t DEVICE_TYPE_LOW_BITS_SHIFT
static constexpr uint8_t IV_PADDING
Padding byte used in IV construction.
Definition proto_frame.h:86
static constexpr uint8_t CMD_CHALLENGE_RESP
Controller responds with HMAC proof.
static constexpr uint8_t CMD_GET_INFO2
Request device type/model info.
static constexpr uint16_t LONG_PREAMBLE
Preamble is a sequence of 0xAA bytes that precedes every frame.
Definition proto_frame.h:39
void decode_position_report(uint16_t target_raw, uint16_t current_raw, bool is_stopped, float &target, float &position)
Decode target/current position values from a status frame.
static constexpr int32_t RESPONSE_AUTH_WAIT_MS
Wait for final response after challenge response.
Definition proto_frame.h:63
static constexpr uint8_t CTRL1_LOW_POWER
Control byte 1 (CTRL1) bit definitions.
static constexpr uint16_t CRC_POLYNOMIAL_REVERSED
Reversed CRC-CCITT polynomial used by IO-homecontrol.
bool device_supports_tilt(DeviceType type)
Does this device type support tilt (slat angle) control?
static constexpr uint8_t STATUS_STOPPED
Status byte flags in CMD_PRIVATE_RESP and CMD_STATUS_UPDATE.
static constexpr int32_t RESPONSE_CHANNEL_WAIT_MS
Per-channel dwell while waiting for an exchange response.
Definition proto_frame.h:60
bool hex_to_bytes(const std::string &hex, uint8_t *out, uint8_t len)
Convert a hex string (e.g., "123ABC") to a byte array.
static constexpr int16_t LBT_RSSI_THRESHOLD_DBM
Listen-before-talk (LBT) parameters for ETSI EN 300 220 compliance.
Definition proto_frame.h:72
static constexpr uint8_t IV_SIZE
Initialization vector size for AES.
Definition proto_frame.h:85
uint8_t serialize(const IoFrame &f, uint8_t *buf, uint8_t buf_size)
Serialize a parsed frame into a wire buffer (without CRC).
void set_src(IoFrame &f, const uint8_t id[NODE_ID_SIZE])
Set source node ID.
static constexpr uint16_t SX1262_AUTH_RESPONSE_PREAMBLE
SX1262-specific preamble for the outbound 0x3D challenge response.
Definition proto_frame.h:48
static constexpr int32_t RESPONSE_START_WAIT_MS
Wait for response to start frame (longer).
Definition proto_frame.h:62
Runtime state of a paired IO‑Homecontrol device.
float target
Target position the device is moving toward.
float tilt
Current tilt: 0=closed, 100=open, or UNKNOWN_POSITION.
uint32_t status_poll_interval_ms
Configured follow-up poll interval while a state change is expected.
uint32_t next_update
millis() timestamp when we should poll for status next.
uint32_t last_status
millis() timestamp of last received status.
char name[DEVICE_NAME_BUFFER_SIZE]
Device name (from device, Latin‑1 encoded).
uint8_t status_poll_failures
Consecutive background status-poll failures without a valid reply.
uint8_t auth_poll_failures
Consecutive background poll failures that reached 0x3C auth challenge.
bool inverted
True if open/close positions are swapped (e.g., horizontal awning).
float position
Current position: 0=open, 100=closed, or UNKNOWN_POSITION.
uint8_t subtype
Device subtype (manufacturer‑specific).
uint8_t node_id[NODE_ID_SIZE]
Device's 3‑byte radio address.
bool single_follow_up_poll_pending
True when one legacy settle poll should still be scheduled.
DeviceType type
Device type (shutter, awning, etc.).
bool is_stopped
True if device is not moving.
uint32_t poll_deadline
Hard stop for bounded follow-up polling after a command or remote activity.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
uint8_t data[FRAME_MAX_DATA_SIZE]
Command parameters (0–23 bytes).
uint8_t ctrl0
Control byte 0: flags + length.
uint8_t src[NODE_ID_SIZE]
Source node ID (3 bytes).
uint8_t dst[NODE_ID_SIZE]
Destination node ID (3 bytes).
uint8_t data_len
Actual length of data.
uint8_t ctrl1
Control byte 1: low power, beacon, etc.