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/// @ingroup hioc_protocol
4
5#include "proto_frame.h"
6
7#include <cctype>
8#include <cmath>
9#include <cstdlib>
10#include <cstring>
11
12namespace esphome {
13namespace home_io_control {
14
15namespace {
16
17constexpr int HEX_ALPHA_OFFSET = 10;
18constexpr uint8_t UTF8_SINGLE_BYTE_MAX = 0x80;
19constexpr uint8_t UTF8_TWO_BYTE_LEAD_BASE = 0xC0;
20constexpr uint8_t UTF8_CONTINUATION_BASE = 0x80;
21constexpr uint8_t UTF8_CONTINUATION_MASK = 0x3F;
22constexpr uint8_t UTF8_TWO_BYTE_SHIFT = 6;
23constexpr uint8_t NAME_PADDING_NUL = 0x00;
24constexpr uint8_t NAME_PADDING_SPACE = 0x20;
25constexpr uint8_t UTF8_TWO_BYTE_MASK = 0xE0;
26constexpr uint8_t UTF8_TWO_BYTE_PREFIX = 0xC0;
27constexpr uint8_t UTF8_THREE_BYTE_MASK = 0xF0;
28constexpr uint8_t UTF8_THREE_BYTE_PREFIX = 0xE0;
29constexpr uint8_t UTF8_FOUR_BYTE_MASK = 0xF8;
30constexpr uint8_t UTF8_FOUR_BYTE_PREFIX = 0xF0;
31constexpr uint8_t UTF8_CONTINUATION_PREFIX_MASK = 0xC0;
32constexpr uint8_t UTF8_CONTINUATION_PREFIX = 0x80;
33constexpr uint8_t UTF8_TWO_BYTE_VALUE_MASK = 0x1F;
34constexpr uint8_t ASCII_MAX = 0x7F;
35
36} // namespace
37
38std::string trim_ascii_whitespace(const std::string &value) {
39 size_t begin = 0;
40 while (begin < value.length() && std::isspace(static_cast<unsigned char>(value[begin])) != 0)
41 begin++;
42
43 size_t end = value.length();
44 while (end > begin && std::isspace(static_cast<unsigned char>(value[end - 1])) != 0)
45 end--;
46
47 return value.substr(begin, end - begin);
48}
49
50namespace {
51
52std::string latin1_to_utf8(const uint8_t *data, size_t len) {
53 std::string result;
54 result.reserve(len * 2);
55
56 for (size_t index = 0; index < len; index++) {
57 uint8_t const byte = data[index];
58 if (byte < UTF8_SINGLE_BYTE_MAX) {
59 if (result.length() + 1 >= DEVICE_NAME_BUFFER_SIZE)
60 break;
61 result.push_back(static_cast<char>(byte));
62 continue;
63 }
64
65 if (result.length() + 2 >= DEVICE_NAME_BUFFER_SIZE)
66 break;
67
68 result.push_back(static_cast<char>(UTF8_TWO_BYTE_LEAD_BASE | (byte >> UTF8_TWO_BYTE_SHIFT)));
69 result.push_back(static_cast<char>(UTF8_CONTINUATION_BASE | (byte & UTF8_CONTINUATION_MASK)));
70 }
71
72 return result;
73}
74
75} // namespace
76
77static int hex_nibble(char ch) {
78 if (ch >= '0' && ch <= '9')
79 return ch - '0';
80 ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
81 if (ch >= 'A' && ch <= 'F')
82 return HEX_ALPHA_OFFSET + (ch - 'A');
83 return -1;
84}
85
86bool hex_to_bytes(const std::string &hex, uint8_t *out, uint8_t len) {
87 if (out == nullptr)
88 return false;
89
90 memset(out, 0, len);
91 if (hex.length() != static_cast<size_t>(len) * 2)
92 return false;
93
94 for (uint8_t i = 0; i < len; i++) {
95 const int high = hex_nibble(hex[i * 2]);
96 const int low = hex_nibble(hex[i * 2 + 1]);
97 if (high < 0 || low < 0)
98 return false;
99 out[i] = static_cast<uint8_t>((high << 4) | low);
100 }
101
102 return true;
103}
104
105std::string node_id_to_string(const uint8_t id[NODE_ID_SIZE]) {
106 char buf[NODE_ID_STRING_SIZE];
107 snprintf(buf, sizeof(buf), "%02X%02X%02X", id[0], id[1], id[2]);
108 return std::string(buf);
109}
110
111std::string decode_device_name_payload(const uint8_t *data, uint8_t len) {
112 if (data == nullptr || len == 0)
113 return {};
114
115 const uint8_t begin = data[0] > NAME_PADDING_SPACE ? 0 : 1;
116 if (begin >= len)
117 return {};
118
119 size_t raw_len = len - begin;
120 while (raw_len > 0 &&
121 (data[begin + raw_len - 1] == NAME_PADDING_NUL || data[begin + raw_len - 1] == NAME_PADDING_SPACE))
122 raw_len--;
123
124 if (raw_len == 0)
125 return {};
126
127 return latin1_to_utf8(data + begin, raw_len);
128}
129
131 uint8_t payload[DEVICE_NAME_WRITE_PAYLOAD_SIZE],
132 std::string &normalized_name) {
133 if (payload == nullptr)
135
136 std::memset(payload, 0, DEVICE_NAME_WRITE_PAYLOAD_SIZE);
137 normalized_name.clear();
138
139 const std::string trimmed_name = trim_ascii_whitespace(name);
140 if (trimmed_name.empty())
142
143 uint8_t latin1_len = 0;
144 for (size_t index = 0; index < trimmed_name.length();) {
145 const auto byte = static_cast<uint8_t>(trimmed_name[index]);
146 uint16_t codepoint = 0;
147 size_t advance = 1;
148
149 if (byte <= ASCII_MAX) {
150 codepoint = byte;
151 } else if ((byte & UTF8_TWO_BYTE_MASK) == UTF8_TWO_BYTE_PREFIX) {
152 if (index + 1 >= trimmed_name.length())
154
155 const auto continuation = static_cast<uint8_t>(trimmed_name[index + 1]);
156 if ((continuation & UTF8_CONTINUATION_PREFIX_MASK) != UTF8_CONTINUATION_PREFIX)
158
159 codepoint = static_cast<uint16_t>(((byte & UTF8_TWO_BYTE_VALUE_MASK) << UTF8_TWO_BYTE_SHIFT) |
160 (continuation & UTF8_CONTINUATION_MASK));
161 if (codepoint < UTF8_SINGLE_BYTE_MAX)
163 advance = 2;
164 } else if ((byte & UTF8_THREE_BYTE_MASK) == UTF8_THREE_BYTE_PREFIX ||
165 (byte & UTF8_FOUR_BYTE_MASK) == UTF8_FOUR_BYTE_PREFIX) {
167 } else {
169 }
170
171 if (codepoint > LATIN1_CODEPOINT_MAX)
173
174 if (latin1_len >= DEVICE_NAME_WRITE_CHAR_LIMIT)
176
177 payload[latin1_len++] = static_cast<uint8_t>(codepoint);
178 index += advance;
179 }
180
181 normalized_name = latin1_to_utf8(payload, latin1_len);
183}
184
186 switch (error) {
188 return "NONE";
190 return "EMPTY";
192 return "TOO_LONG";
194 return "INVALID_UTF8";
196 return "UNSUPPORTED_CHAR";
197 default:
198 return "UNKNOWN_DEVICE_NAME_VALIDATION_ERROR";
199 }
200}
201
203 switch (error) {
205 return "name accepted";
207 return "device name must not be empty";
209 return "device name exceeds the 15-character write limit";
211 return "device name must be valid UTF-8";
213 return "device name contains characters outside Latin-1";
214 default:
215 return "unknown device-name validation error";
216 }
217}
218
220
221const char *command_result_name(uint8_t result) {
222 switch (result) {
224 return "UNKNOWN_STATUS_REPLY";
226 return "COMMAND_COMPLETED_OK";
228 return "NO_CONTACT";
230 return "MANUALLY_OPERATED";
231 case RESULT_BLOCKED:
232 return "BLOCKED";
234 return "WRONG_SYSTEMKEY";
236 return "PRIORITY_LEVEL_LOCKED";
238 return "REACHED_WRONG_POSITION";
240 return "ERROR_DURING_EXECUTION";
242 return "NO_EXECUTION";
244 return "CALIBRATING";
246 return "POWER_CONSUMPTION_TOO_HIGH";
248 return "POWER_CONSUMPTION_TOO_LOW";
250 return "LOCK_POSITION_OPEN";
252 return "MOTION_TIME_TOO_LONG";
254 return "THERMAL_PROTECTION";
256 return "PRODUCT_NOT_OPERATIONAL";
258 return "FILTER_MAINTENANCE_NEEDED";
260 return "BATTERY_LEVEL";
262 return "TARGET_MODIFIED";
264 return "MODE_NOT_IMPLEMENTED";
266 return "COMMAND_INCOMPATIBLE_TO_MOVEMENT";
268 return "USER_ACTION";
270 return "DEAD_BOLT_ERROR";
272 return "AUTOMATIC_CYCLE_ENGAGED";
274 return "WRONG_LOAD_CONNECTED";
276 return "COLOUR_NOT_REACHABLE";
278 return "TARGET_NOT_REACHABLE";
280 return "BAD_INDEX_RECEIVED";
282 return "COMMAND_OVERRULED";
284 return "NODE_WAITING_FOR_POWER";
286 return "NODE_LOCKED";
288 return "WRONG_POSITION";
290 return "LIMITS_NOT_SET";
292 return "IP_NOT_SET";
294 return "OUT_OF_RANGE";
296 return "INFORMATION_CODE";
298 return "PARAMETER_LIMITED";
300 return "LIMITATION_BY_LOCAL_USER";
302 return "LIMITATION_BY_USER";
304 return "LIMITATION_BY_RAIN";
306 return "LIMITATION_BY_TIMER";
308 return "LIMITATION_BY_SCD";
310 return "LIMITATION_BY_UPS";
312 return "LIMITATION_BY_UNKNOWN_DEVICE";
314 return "LIMITATION_BY_SAAC";
316 return "LIMITATION_BY_WIND";
318 return "LIMITATION_BY_MYSELF";
320 return "LIMITATION_BY_AUTOMATIC_CYCLE";
322 return "LIMITATION_BY_EMERGENCY";
323 default:
324 return "UNKNOWN_RESULT_CODE";
325 }
326}
327
328const char *command_result_description(uint8_t result) {
329 switch (result) {
331 return "unknown reply";
333 return "no errors detected";
335 return "no communication to node";
337 return "manually operated by a user";
338 case RESULT_BLOCKED:
339 return "node has been blocked by an object";
341 return "node contains the wrong system key";
343 return "node is locked on this priority level";
345 return "node stopped in another position than expected";
347 return "an error occurred during command execution";
349 return "no movement of the node parameter";
351 return "node is calibrating the parameters";
353 return "node power consumption is too high";
355 return "node power consumption is too low";
357 return "door open during lock command";
359 return "target was not reached in time";
361 return "node has gone into thermal protection mode";
363 return "node is not currently operational";
365 return "filter needs maintenance";
367 return "battery level is low";
369 return "node modified the requested target value";
371 return "node does not support the received mode";
373 return "node cannot move in the requested direction";
375 return "user action overrode the command";
377 return "dead bolt error";
379 return "node has gone into automatic cycle mode";
381 return "wrong load connected to node";
383 return "node cannot reach the requested colour";
385 return "node cannot reach the requested target position";
387 return "invalid index received";
389 return "command was overruled by a newer command";
391 return "node is waiting for power";
393 return "node is locked";
395 return "wrong position";
397 return "limits are not set";
399 return "intermediate position is not set";
401 return "requested value is out of range";
403 return "information-only result with unknown semantics";
405 return "parameter was limited by an unknown device";
407 return "parameter was limited by the local button";
409 return "parameter was limited by a remote control";
411 return "parameter was limited by a rain sensor";
413 return "parameter was limited by a timer";
415 return "parameter was limited by a security controlling actuator";
417 return "parameter was limited by a power supply";
419 return "parameter was limited by an unknown device";
421 return "parameter was limited by a standalone automatic controller";
423 return "parameter was limited by a wind sensor";
425 return "parameter was limited by the node itself";
427 return "parameter was limited by an automatic cycle";
429 return "parameter was limited by an emergency";
430 default:
431 return "unknown result code";
432 }
433}
434
455
456DeviceType decode_packed_device_type(uint8_t type_msb, uint8_t type_subtype) {
457 return static_cast<DeviceType>((type_msb << DEVICE_TYPE_LOW_BITS_SHIFT) |
458 (type_subtype >> DEVICE_TYPE_HIGH_BITS_SHIFT));
459}
460
461uint8_t decode_packed_device_subtype(uint8_t type_subtype) { return type_subtype & DEVICE_SUBTYPE_MASK; }
462
463void decode_position_report(uint16_t target_raw, uint16_t current_raw, bool is_stopped, float &target,
464 float &position) {
465 bool const target_valid = target_raw <= STATUS_POS_MAX;
466 bool const current_valid = current_raw <= STATUS_POS_MAX;
467 float const decoded_current = current_valid ? current_raw * 100.0F / STATUS_POS_MAX : UNKNOWN_POSITION;
468
469 if (target_valid) {
470 target = target_raw * 100.0F / STATUS_POS_MAX;
471 } else if (is_stopped && current_valid) {
472 // Marker values such as D2 (stop) and D4 (keep position during tilt) exceed STATUS_POS_MAX.
473 // When the device says it is stopped and still gives a valid current position, use that as
474 // the effective target instead of discarding the target entirely.
475 target = decoded_current;
476 } else {
477 target = UNKNOWN_POSITION;
478 }
479
480 if (current_valid) {
481 position = decoded_current;
482 } else if (is_stopped && target_valid) {
483 position = target;
484 } else {
485 position = UNKNOWN_POSITION;
486 }
487}
488
489bool has_reached_target_position(float target, float position) {
490 if (target == UNKNOWN_POSITION || position == UNKNOWN_POSITION)
491 return false;
492 float const tolerance = STATUS_POS_TOLERANCE_RAW * 100.0F / STATUS_POS_MAX;
493 return std::fabs(target - position) <= tolerance;
494}
495
496float decode_tilt_report(uint16_t tilt_raw) {
497 if (tilt_raw > STATUS_POS_MAX)
498 return UNKNOWN_POSITION;
499 return 100.0F - (tilt_raw * 100.0F / STATUS_POS_MAX);
500}
501
502/// CRC-CCITT used by the IO-Homecontrol protocol for frame validation.
503/// Polynomial: 0x1021 (reversed 0x8408), initial value: 0x0000.
504/// On SX1276 this is computed in hardware (IoHomeOn mode); on SX1262 it is
505/// computed in software by the radio driver.
506uint16_t crc_ccitt(const uint8_t *data, uint8_t len) {
507 uint16_t crc = 0x0000;
508 for (uint8_t i = 0; i < len; i++) {
509 crc ^= data[i];
510 for (uint8_t j = 0; j < BITS_PER_BYTE; j++)
511 crc = ((crc & CRC_LSB_MASK) != 0) ? (crc >> 1) ^ CRC_POLYNOMIAL_REVERSED : crc >> 1;
512 }
513 return crc;
514}
515
516void init_frame(IoFrame &f, bool is_2w, bool start, bool end, bool low_power) {
517 memset(&f, 0, sizeof(IoFrame));
518 if (end)
519 f.ctrl0 |= CTRL0_END;
520 if (start)
521 f.ctrl0 |= CTRL0_START;
522 if (!is_2w)
524 if (low_power)
526}
527
528void set_dst(IoFrame &f, const uint8_t id[NODE_ID_SIZE]) { memcpy(f.dst, id, NODE_ID_SIZE); }
529void set_src(IoFrame &f, const uint8_t id[NODE_ID_SIZE]) { memcpy(f.src, id, NODE_ID_SIZE); }
530
531bool set_cmd(IoFrame &f, uint8_t cmd, const uint8_t *params, uint8_t params_len) {
532 if (params_len > FRAME_MAX_DATA_SIZE)
533 return false;
534 f.cmd = cmd;
535 f.data_len = params_len;
536 if (params != nullptr && params_len > 0)
537 memcpy(f.data, params, params_len);
538 uint8_t const total = FRAME_MIN_SIZE + f.data_len;
539 // Refuse to encode inconsistent frame metadata here so malformed commands never make it onto
540 // the radio path and later confuse the serializer or on-air retries.
541 if (total > FRAME_MAX_SIZE)
542 return false;
543 f.ctrl0 = (f.ctrl0 & ~CTRL0_LENGTH_MASK) | ((total - 1) & CTRL0_LENGTH_MASK);
544 return true;
545}
546
547uint8_t frame_length(const IoFrame &f) { return (f.ctrl0 & CTRL0_LENGTH_MASK) + 1; }
548bool is_start(const IoFrame &f) { return (f.ctrl0 & CTRL0_START) != 0; }
549bool is_end(const IoFrame &f) { return (f.ctrl0 & CTRL0_END) != 0; }
550
551uint8_t serialize(const IoFrame &f, uint8_t *buf, uint8_t buf_size) {
552 if (buf == nullptr)
553 return 0;
554 uint8_t const len = frame_length(f);
555 if (len < FRAME_MIN_SIZE || len > FRAME_MAX_SIZE)
556 return 0;
558 return 0;
559 // Keep the wire length derived from ctrl0 and the explicit payload length in lockstep. This
560 // catches partially initialized frames before they are transmitted.
561 if ((uint8_t) (FRAME_MIN_SIZE + f.data_len) != len)
562 return 0;
563 if (buf_size < len)
564 return 0;
565 uint8_t offset = 0;
566 buf[offset++] = f.ctrl0;
567 buf[offset++] = f.ctrl1;
568 memcpy(&buf[offset], f.dst, NODE_ID_SIZE);
569 offset += NODE_ID_SIZE;
570 memcpy(&buf[offset], f.src, NODE_ID_SIZE);
571 offset += NODE_ID_SIZE;
572 buf[offset++] = f.cmd;
573 memcpy(&buf[offset], f.data, f.data_len);
574 offset += f.data_len;
575 return offset;
576}
577
578bool parse(const uint8_t *buf, uint8_t buf_len, IoFrame &f) {
579 if (buf == nullptr)
580 return false;
581 if (buf_len < FRAME_MIN_SIZE)
582 return false;
583 memset(&f, 0, sizeof(IoFrame));
584 uint8_t offset = 0;
585 f.ctrl0 = buf[offset++];
586 f.ctrl1 = buf[offset++];
587 uint8_t const len = frame_length(f);
588 if (len < FRAME_MIN_SIZE || len > FRAME_MAX_SIZE)
589 return false;
590 if (buf_len != len)
591 return false;
592 if (offset + NODE_ID_SIZE > buf_len)
593 return false;
594 memcpy(f.dst, &buf[offset], NODE_ID_SIZE);
595 offset += NODE_ID_SIZE;
596 if (offset + NODE_ID_SIZE > buf_len)
597 return false;
598 memcpy(f.src, &buf[offset], NODE_ID_SIZE);
599 offset += NODE_ID_SIZE;
600 if (offset >= buf_len)
601 return false;
602 f.cmd = buf[offset++];
603 f.data_len = len - FRAME_MIN_SIZE;
605 return false;
606 if (offset + f.data_len > buf_len)
607 return false;
608 memcpy(f.data, &buf[offset], f.data_len);
609 return true;
610}
611
612const char *device_type_name(DeviceType type) {
613 switch (type) {
615 return "unknown";
617 return "venetian_blind";
619 return "roller_shutter";
621 return "screen";
623 return "awning";
625 return "window_opener";
627 return "garage_opener";
629 return "light";
631 return "gate_opener";
633 return "rolling_door_opener";
635 return "blind";
637 return "dual_shutter";
639 return "on_off_switch";
641 return "horizontal_awning";
643 return "external_venetian_blind";
645 return "louvre_blind";
647 return "curtain_track";
649 return "swinging_shutter";
650 case DeviceType::LOCK:
651 return "lock";
653 return "beacon";
655 return "heating_temperature_interface";
657 return "ventilation_point";
659 return "exterior_heating";
661 return "heat_pump";
663 return "intrusion_alarm";
664 }
665
666 return "unknown";
667}
668
670 switch (type) {
671 // Cover types (position-controlled)
688
689 // Binary and other capabilities
694 case DeviceType::LOCK:
701 // Binary ventilation on/off; treated as switch
707
709 default:
711 }
712}
713
715 switch (device_capability_class(type)) {
717 return "cover";
719 return "light";
721 return "switch";
723 return "sensor";
725 return "beacon";
727 return "climate";
729 return "lock";
731 default:
732 return "unknown";
733 }
734}
735
739
741 DeviceCapabilityClass const capability_class = device_capability_class(type);
742 return capability_class == DeviceCapabilityClass::LIGHT || capability_class == DeviceCapabilityClass::SWITCH;
743}
744
748
753
755 switch (type) {
760 return true;
761 default:
762 return false;
763 }
764}
765
768 return device_supports_tilt(type) ? "cover_position_tilt" : "cover_position";
770 return "binary_on_off";
771
772 switch (device_capability_class(type)) {
774 return "lock";
776 return "climate";
778 return "sensor";
780 return "beacon";
782 default:
783 return "unknown";
784 }
785}
786
787} // namespace home_io_control
788} // 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 RESULT_OUT_OF_RANGE
Requested value is out of range.
static constexpr uint8_t RESULT_CALIBRATING
Node is calibrating.
static constexpr uint8_t BITS_PER_BYTE
Number of bits in one protocol byte.
Definition proto_frame.h:88
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:81
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 RESULT_COLOUR_NOT_REACHABLE
Requested colour not reachable.
static constexpr uint8_t RESULT_LIMITATION_BY_AUTOMATIC_CYCLE
Parameter limited by an automatic cycle.
static constexpr uint8_t RESULT_TARGET_MODIFIED
Node modified the requested target value.
static constexpr uint8_t FRAME_MIN_SIZE
Minimum frame: CTRL0+CTRL1+DST(3)+SRC(3)+CMD(1).
Definition proto_frame.h:90
static constexpr uint8_t RESULT_REACHED_WRONG_POSITION
Node stopped in another position than expected.
static constexpr uint8_t RESULT_LIMITATION_BY_MYSELF
Parameter limited by the node itself.
static constexpr uint8_t RESULT_AUTOMATIC_CYCLE_ENGAGED
Node entered automatic cycle mode.
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 RESULT_MODE_NOT_IMPLEMENTED
Mode is not supported by the node.
static constexpr uint8_t RESULT_COMMAND_OVERRULED
Command was overruled by a newer command.
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:92
static constexpr uint8_t RESULT_BAD_INDEX_RECEIVED
Invalid index received.
static constexpr uint8_t RESULT_MANUALLY_OPERATED
Manually operated by a user.
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.
static constexpr uint16_t LATIN1_CODEPOINT_MAX
Highest Unicode code point representable in Latin-1.
static constexpr uint8_t RESULT_LIMITATION_BY_UNKNOWN_DEVICE
Parameter limited by an unknown device.
bool is_start(const IoFrame &f)
Check START flag.
static constexpr uint8_t RESULT_LOCK_POSITION_OPEN
Lock command failed because the door is open.
bool device_supports_position_control(DeviceType type)
Does this device type support precise position control (0–100)?
static constexpr uint8_t RESULT_WRONG_SYSTEMKEY
Node contains the wrong system key.
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 RESULT_LIMITATION_BY_EMERGENCY
Parameter limited by an emergency.
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 RESULT_NODE_WAITING_FOR_POWER
Node is waiting for power.
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 RESULT_BATTERY_LEVEL
Battery level is low.
static constexpr uint8_t RESULT_MOTION_TIME_TOO_LONG
Target was not reached in time.
float decode_tilt_report(uint16_t tilt_raw)
Decode tilt angle from raw 16‑bit value.
static constexpr uint8_t RESULT_THERMAL_PROTECTION
Node entered thermal protection mode.
static constexpr uint8_t RESULT_POWER_CONSUMPTION_TOO_HIGH
Node power consumption is too high.
static constexpr uint8_t RESULT_ERROR_DURING_EXECUTION
Generic execution failure.
uint8_t frame_length(const IoFrame &f)
Get total frame length from ctrl0.
bool device_supports_lock_control(DeviceType type)
Does this device type support binary lock/unlock control via execute commands?
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 RESULT_PARAMETER_LIMITED
Parameter limited by an unknown device.
static constexpr uint8_t RESULT_LIMITATION_BY_RAIN
Parameter limited by a rain sensor.
static constexpr uint8_t RESULT_USER_ACTION
User action overrode the command.
const char * command_result_description(uint8_t result)
Return a human-readable explanation for a CMD_ERROR_RESP result code.
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.
const char * device_name_validation_error_name(DeviceNameValidationError error)
Return a stable symbolic name for a device-name validation result.
const char * command_result_name(uint8_t result)
Return a stable symbolic name for a CMD_ERROR_RESP result code.
static constexpr uint8_t NODE_ID_STRING_SIZE
Uppercase hex node ID plus null terminator.
Definition proto_frame.h:82
bool is_end(const IoFrame &f)
Check END flag.
DeviceNameValidationError
Validation result for outbound device-name writes.
@ UNSUPPORTED_CHAR
Name contains characters outside Latin-1.
@ TOO_LONG
Name exceeds the 15-character write limit.
@ EMPTY
Name is empty after normalization.
@ INVALID_UTF8
Name contains malformed UTF-8 bytes.
static constexpr uint8_t RESULT_DEAD_BOLT_ERROR
Dead bolt error.
static constexpr uint8_t RESULT_NO_EXECUTION
Node did not move.
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).
std::string trim_ascii_whitespace(const std::string &value)
Trim leading and trailing ASCII whitespace from a string.
static constexpr uint8_t RESULT_LIMITATION_BY_WIND
Parameter limited by a wind sensor.
static constexpr uint8_t RESULT_LIMITATION_BY_UPS
Parameter limited by a power supply.
const char * device_capability_class_name(DeviceType type)
Get a human‑readable name for a capability class.
static constexpr uint8_t RESULT_LIMITATION_BY_USER
Parameter limited by a remote control.
static constexpr uint8_t RESULT_COMMAND_COMPLETED_OK
No errors detected.
static constexpr uint8_t RESULT_WRONG_POSITION
Node reports wrong position.
static constexpr uint8_t DEVICE_NAME_WRITE_CHAR_LIMIT
Reference write limit before the trailing null.
static constexpr uint8_t RESULT_COMMAND_INCOMPATIBLE_TO_MOVEMENT
Command cannot move the node that way.
DeviceNameValidationError encode_device_name_payload(const std::string &name, uint8_t payload[DEVICE_NAME_WRITE_PAYLOAD_SIZE], std::string &normalized_name)
Validate and encode a user-supplied UTF-8 device name into the fixed Latin-1 write payload.
static constexpr uint8_t RESULT_PRODUCT_NOT_OPERATIONAL
Node is not currently operational.
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.
std::string decode_device_name_payload(const uint8_t *data, uint8_t len)
Decode a device-name payload from IO-homecontrol's Latin-1 wire format into UTF-8.
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 RESULT_POWER_CONSUMPTION_TOO_LOW
Node power consumption is too low.
static constexpr uint8_t RESULT_FILTER_MAINTENANCE_NEEDED
Filter needs maintenance.
static constexpr uint8_t RESULT_WRONG_LOAD_CONNECTED
Wrong load connected to node.
bool device_supports_status_requests(DeviceType type)
Does this device type support status request commands (0x03)?
static constexpr uint8_t RESULT_LIMITATION_BY_SAAC
Parameter limited by a standalone automatic controller.
static constexpr uint8_t DEVICE_SUBTYPE_MASK
static constexpr uint8_t RESULT_LIMITATION_BY_TIMER
Parameter limited by a timer.
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 RESULT_PRIORITY_LEVEL_LOCKED
Node is locked on this priority level.
static constexpr uint8_t DEVICE_TYPE_LOW_BITS_SHIFT
bool is_limitation_result(uint8_t result)
Check whether a result code represents an environmental or control limitation.
static constexpr uint8_t RESULT_UNKNOWN_STATUS_REPLY
Device returned an unknown status reply.
static int hex_nibble(char ch)
static constexpr uint8_t RESULT_LIMITS_NOT_SET
Device limits are not set.
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 RESULT_NODE_LOCKED
Node is locked.
static constexpr uint8_t CTRL1_LOW_POWER
Control byte 1 (CTRL1) bit definitions.
const char * device_name_validation_error_description(DeviceNameValidationError error)
Return a human-readable explanation for a device-name validation result.
static constexpr uint16_t CRC_POLYNOMIAL_REVERSED
Reversed CRC-CCITT polynomial used by IO-homecontrol.
static constexpr uint8_t RESULT_NO_CONTACT
No communication to node.
bool device_supports_tilt(DeviceType type)
Does this device type support tilt (slat angle) control?
static constexpr uint8_t RESULT_BLOCKED
Node blocked by an object.
static constexpr uint8_t RESULT_INFORMATION_CODE
Information-only code with unknown semantics.
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 uint8_t RESULT_LIMITATION_BY_LOCAL_USER
Parameter limited by local button.
uint8_t serialize(const IoFrame &f, uint8_t *buf, uint8_t buf_size)
Serialize a parsed frame into a wire buffer (without CRC).
static constexpr uint8_t RESULT_TARGET_NOT_REACHABLE
Requested target not reachable.
static constexpr uint8_t RESULT_IP_NOT_SET
Intermediate position is not set.
void set_src(IoFrame &f, const uint8_t id[NODE_ID_SIZE])
Set source node ID.
static constexpr uint8_t RESULT_LIMITATION_BY_SCD
Parameter limited by a security actuator.
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.