Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
proto_frame.cpp
Go to the documentation of this file.
1/// @file proto_frame.cpp
2/// @brief IO-Homecontrol 2W protocol implementation.
3
4#include "proto_frame.h"
5
6#include <cctype>
7#include <cmath>
8#include <cstdlib>
9
10namespace esphome {
11namespace home_io_control {
12
13namespace {
14
15constexpr int HEX_ALPHA_OFFSET = 10;
16
17} // namespace
18
19static int hex_nibble(char ch) {
20 if (ch >= '0' && ch <= '9')
21 return ch - '0';
22 ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
23 if (ch >= 'A' && ch <= 'F')
24 return HEX_ALPHA_OFFSET + (ch - 'A');
25 return -1;
26}
27
28bool hex_to_bytes(const std::string &hex, uint8_t *out, uint8_t len) {
29 if (out == nullptr)
30 return false;
31
32 memset(out, 0, len);
33 if (hex.length() != static_cast<size_t>(len) * 2)
34 return false;
35
36 for (uint8_t i = 0; i < len; i++) {
37 const int high = hex_nibble(hex[i * 2]);
38 const int low = hex_nibble(hex[i * 2 + 1]);
39 if (high < 0 || low < 0)
40 return false;
41 out[i] = static_cast<uint8_t>((high << 4) | low);
42 }
43
44 return true;
45}
46
47std::string node_id_to_string(const uint8_t id[NODE_ID_SIZE]) {
48 char buf[NODE_ID_STRING_SIZE];
49 snprintf(buf, sizeof(buf), "%02X%02X%02X", id[0], id[1], id[2]);
50 return std::string(buf);
51}
52
54
55DeviceType decode_packed_device_type(uint8_t type_msb, uint8_t type_subtype) {
56 return static_cast<DeviceType>((type_msb << DEVICE_TYPE_LOW_BITS_SHIFT) |
57 (type_subtype >> DEVICE_TYPE_HIGH_BITS_SHIFT));
58}
59
60uint8_t decode_packed_device_subtype(uint8_t type_subtype) { return type_subtype & DEVICE_SUBTYPE_MASK; }
61
62void decode_position_report(uint16_t target_raw, uint16_t current_raw, bool is_stopped, float &target,
63 float &position) {
64 bool const target_valid = target_raw <= STATUS_POS_MAX;
65 bool const current_valid = current_raw <= STATUS_POS_MAX;
66 float const decoded_current = current_valid ? current_raw * 100.0F / STATUS_POS_MAX : UNKNOWN_POSITION;
67
68 if (target_valid) {
69 target = target_raw * 100.0F / STATUS_POS_MAX;
70 } else if (is_stopped && current_valid) {
71 // Marker values such as D2 (stop) and D4 (keep position during tilt) exceed STATUS_POS_MAX.
72 // When the device says it is stopped and still gives a valid current position, use that as
73 // the effective target instead of discarding the target entirely.
74 target = decoded_current;
75 } else {
76 target = UNKNOWN_POSITION;
77 }
78
79 if (current_valid) {
80 position = decoded_current;
81 } else if (is_stopped && target_valid) {
82 position = target;
83 } else {
84 position = UNKNOWN_POSITION;
85 }
86}
87
88bool has_reached_target_position(float target, float position) {
89 if (target == UNKNOWN_POSITION || position == UNKNOWN_POSITION)
90 return false;
91 float const tolerance = STATUS_POS_TOLERANCE_RAW * 100.0F / STATUS_POS_MAX;
92 return std::fabs(target - position) <= tolerance;
93}
94
95float decode_tilt_report(uint16_t tilt_raw) {
96 if (tilt_raw > STATUS_POS_MAX)
97 return UNKNOWN_POSITION;
98 return 100.0F - (tilt_raw * 100.0F / STATUS_POS_MAX);
99}
100
101/// CRC-CCITT used by the IO-Homecontrol protocol for frame validation.
102/// Polynomial: 0x1021 (reversed 0x8408), initial value: 0x0000.
103/// On SX1276 this is computed in hardware (IoHomeOn mode); on SX1262 it is
104/// computed in software by the radio driver.
105uint16_t crc_ccitt(const uint8_t *data, uint8_t len) {
106 uint16_t crc = 0x0000;
107 for (uint8_t i = 0; i < len; i++) {
108 crc ^= data[i];
109 for (uint8_t j = 0; j < BITS_PER_BYTE; j++)
110 crc = ((crc & CRC_LSB_MASK) != 0) ? (crc >> 1) ^ CRC_POLYNOMIAL_REVERSED : crc >> 1;
111 }
112 return crc;
113}
114
115void init_frame(IoFrame &f, bool is_2w, bool start, bool end, bool low_power) {
116 memset(&f, 0, sizeof(IoFrame));
117 if (end)
118 f.ctrl0 |= CTRL0_END;
119 if (start)
120 f.ctrl0 |= CTRL0_START;
121 if (!is_2w)
123 if (low_power)
125}
126
127void set_dst(IoFrame &f, const uint8_t id[NODE_ID_SIZE]) { memcpy(f.dst, id, NODE_ID_SIZE); }
128void set_src(IoFrame &f, const uint8_t id[NODE_ID_SIZE]) { memcpy(f.src, id, NODE_ID_SIZE); }
129
130bool set_cmd(IoFrame &f, uint8_t cmd, const uint8_t *params, uint8_t params_len) {
131 if (params_len > FRAME_MAX_DATA_SIZE)
132 return false;
133 f.cmd = cmd;
134 f.data_len = params_len;
135 if (params != nullptr && params_len > 0)
136 memcpy(f.data, params, params_len);
137 uint8_t const total = FRAME_MIN_SIZE + f.data_len;
138 // Refuse to encode inconsistent frame metadata here so malformed commands never make it onto
139 // the radio path and later confuse the serializer or on-air retries.
140 if (total > FRAME_MAX_SIZE)
141 return false;
142 f.ctrl0 = (f.ctrl0 & ~CTRL0_LENGTH_MASK) | ((total - 1) & CTRL0_LENGTH_MASK);
143 return true;
144}
145
146uint8_t frame_length(const IoFrame &f) { return (f.ctrl0 & CTRL0_LENGTH_MASK) + 1; }
147bool is_start(const IoFrame &f) { return (f.ctrl0 & CTRL0_START) != 0; }
148bool is_end(const IoFrame &f) { return (f.ctrl0 & CTRL0_END) != 0; }
149
150uint8_t serialize(const IoFrame &f, uint8_t *buf, uint8_t buf_size) {
151 if (buf == nullptr)
152 return 0;
153 uint8_t const len = frame_length(f);
154 if (len < FRAME_MIN_SIZE || len > FRAME_MAX_SIZE)
155 return 0;
157 return 0;
158 // Keep the wire length derived from ctrl0 and the explicit payload length in lockstep. This
159 // catches partially initialized frames before they are transmitted.
160 if ((uint8_t) (FRAME_MIN_SIZE + f.data_len) != len)
161 return 0;
162 if (buf_size < len)
163 return 0;
164 uint8_t offset = 0;
165 buf[offset++] = f.ctrl0;
166 buf[offset++] = f.ctrl1;
167 memcpy(&buf[offset], f.dst, NODE_ID_SIZE);
168 offset += NODE_ID_SIZE;
169 memcpy(&buf[offset], f.src, NODE_ID_SIZE);
170 offset += NODE_ID_SIZE;
171 buf[offset++] = f.cmd;
172 memcpy(&buf[offset], f.data, f.data_len);
173 offset += f.data_len;
174 return offset;
175}
176
177bool parse(const uint8_t *buf, uint8_t buf_len, IoFrame &f) {
178 if (buf == nullptr)
179 return false;
180 if (buf_len < FRAME_MIN_SIZE)
181 return false;
182 memset(&f, 0, sizeof(IoFrame));
183 uint8_t offset = 0;
184 f.ctrl0 = buf[offset++];
185 f.ctrl1 = buf[offset++];
186 uint8_t const len = frame_length(f);
187 if (len < FRAME_MIN_SIZE || len > FRAME_MAX_SIZE)
188 return false;
189 if (buf_len != len)
190 return false;
191 if (offset + NODE_ID_SIZE > buf_len)
192 return false;
193 memcpy(f.dst, &buf[offset], NODE_ID_SIZE);
194 offset += NODE_ID_SIZE;
195 if (offset + NODE_ID_SIZE > buf_len)
196 return false;
197 memcpy(f.src, &buf[offset], NODE_ID_SIZE);
198 offset += NODE_ID_SIZE;
199 if (offset >= buf_len)
200 return false;
201 f.cmd = buf[offset++];
202 f.data_len = len - FRAME_MIN_SIZE;
204 return false;
205 if (offset + f.data_len > buf_len)
206 return false;
207 memcpy(f.data, &buf[offset], f.data_len);
208 return true;
209}
210
211const char *device_type_name(DeviceType type) {
212 switch (type) {
214 return "unknown";
216 return "venetian_blind";
218 return "roller_shutter";
220 return "screen";
222 return "awning";
224 return "window_opener";
226 return "garage_opener";
228 return "light";
230 return "gate_opener";
232 return "rolling_door_opener";
234 return "blind";
236 return "dual_shutter";
238 return "on_off_switch";
240 return "horizontal_awning";
242 return "external_venetian_blind";
244 return "louvre_blind";
246 return "curtain_track";
248 return "swinging_shutter";
249 case DeviceType::LOCK:
250 return "lock";
252 return "beacon";
254 return "heating_temperature_interface";
256 return "ventilation_point";
258 return "exterior_heating";
260 return "heat_pump";
262 return "intrusion_alarm";
263 }
264
265 return "unknown";
266}
267
269 switch (type) {
270 // Cover types (position-controlled)
287
288 // Binary and other capabilities
293 case DeviceType::LOCK:
300 // Binary ventilation on/off; treated as switch
306
308 default:
310 }
311}
312
314 switch (device_capability_class(type)) {
316 return "cover";
318 return "light";
320 return "switch";
322 return "sensor";
324 return "beacon";
326 return "climate";
328 return "lock";
330 default:
331 return "unknown";
332 }
333}
334
338
340 DeviceCapabilityClass const capability_class = device_capability_class(type);
341 return capability_class == DeviceCapabilityClass::LIGHT || capability_class == DeviceCapabilityClass::SWITCH;
342}
343
347
349 switch (type) {
354 return true;
355 default:
356 return false;
357 }
358}
359
362 return device_supports_tilt(type) ? "cover_position_tilt" : "cover_position";
364 return "binary_on_off";
365
366 switch (device_capability_class(type)) {
368 return "lock";
370 return "climate";
372 return "sensor";
374 return "beacon";
376 default:
377 return "unknown";
378 }
379}
380
381} // namespace home_io_control
382} // namespace esphome
bool set_cmd(IoFrame &f, uint8_t cmd, const uint8_t *params, uint8_t params_len)
Set command and payload.
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 float UNKNOWN_POSITION
Sentinel value meaning "position is not known yet".
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
Definition proto_frame.h:80
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 FRAME_MIN_SIZE
Minimum frame: CTRL0+CTRL1+DST(3)+SRC(3)+CMD(1).
Definition proto_frame.h:89
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 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.
bool default_inverted_for_type(DeviceType type)
Determine whether a device type has inverted position mapping by default.
bool is_start(const IoFrame &f)
Check START flag.
bool device_supports_position_control(DeviceType type)
Does this device type support precise position control (0–100)?
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?
uint16_t crc_ccitt(const uint8_t *data, uint8_t len)
CRC-CCITT used by the IO-Homecontrol protocol for frame validation.
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
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.
bool parse(const uint8_t *buf, uint8_t buf_len, IoFrame &f)
Parse a wire buffer into a parsed IoFrame (validates length and CTRL0).
const char * device_capability_class_name(DeviceType type)
Get a human‑readable name for a capability class.
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.
uint8_t decode_packed_device_subtype(uint8_t type_subtype)
Decode a protocol-packed device subtype from the second metadata byte.
bool device_supports_status_requests(DeviceType type)
Does this device type support status request commands (0x03)?
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?
DeviceCapabilityClass
High‑level capability class derived from DeviceType.
@ CLIMATE
Climate device (heating/cooling).
@ COVER
Position‑controlled cover (shutter/blind/awning).
static constexpr uint8_t DEVICE_TYPE_LOW_BITS_SHIFT
static int hex_nibble(char ch)
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 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?
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.
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.
IO-Homecontrol 2W protocol definitions.
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.