Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
proto_codecs.h
Go to the documentation of this file.
1#pragma once
2
3/// @file proto_codecs.h
4/// @brief Device-name, address-classification and 1W-frame codecs.
5/// @ingroup hioc_protocol
6///
7/// Higher-level decode/encode helpers layered on the frame and device model:
8/// Latin-1 device-name round-tripping, broadcast address classification and
9/// structured decoding of overheard 1W remote frames.
10
11#include "proto_device_model.h"
12#include "proto_sizes.h"
13
14#include <cstddef>
15#include <cstdint>
16#include <optional>
17#include <string>
18
19namespace esphome {
20namespace home_io_control {
21
22struct IoFrame; // Defined in proto_frame.h; decode_1w_frame takes it by reference.
23
24// ============================================================================
25// Device Name Codec
26// ============================================================================
27
28static constexpr uint8_t DEVICE_NAME_WRITE_CHAR_LIMIT = 15; ///< Reference write limit before the trailing null.
29static constexpr uint8_t DEVICE_NAME_WRITE_PAYLOAD_SIZE =
30 DEVICE_NAME_WRITE_CHAR_LIMIT + 1; ///< Fixed write payload: 15 visible chars plus trailing null/padding.
31static constexpr uint16_t LATIN1_CODEPOINT_MAX = 0x00FF; ///< Highest Unicode code point representable in Latin-1.
32
33/// @brief Validation result for outbound device-name writes.
34enum class DeviceNameValidationError : uint8_t {
35 NONE = 0x00, ///< Name is valid and encodable.
36 EMPTY = 0x01, ///< Name is empty after normalization.
37 TOO_LONG = 0x02, ///< Name exceeds the 15-character write limit.
38 INVALID_UTF8 = 0x03, ///< Name contains malformed UTF-8 bytes.
39 UNSUPPORTED_CHAR = 0x04, ///< Name contains characters outside Latin-1.
40};
41
42/// @brief Decode a device-name payload from IO-homecontrol's Latin-1 wire format into UTF-8.
43/// Some devices prepend an extra byte before the first character and many pad the payload with
44/// trailing 0x00 or 0x20 bytes. This helper normalizes those quirks and truncates the result to
45/// fit DEVICE_NAME_BUFFER_SIZE - 1 bytes when copied into IoDevice::name.
46/// @param data Raw payload pointer from CMD_GET_NAME_RESP.
47/// @param len Raw payload length in bytes.
48/// @return Normalized UTF-8 name, or an empty string when the payload carries no usable characters.
49std::string decode_device_name_payload(const uint8_t *data, uint8_t len);
50/// @brief Trim leading and trailing ASCII whitespace from a string.
51/// This is shared by device-name validation and management actions so both paths normalize input
52/// consistently before comparing or writing device metadata.
53/// @param value Input string.
54/// @return Copy of the string with leading and trailing ASCII whitespace removed.
55std::string trim_ascii_whitespace(const std::string &value);
56/// @brief Validate and encode a user-supplied UTF-8 device name into the fixed Latin-1 write payload.
57/// The outbound payload is a fixed 16-byte field: up to 15 Latin-1 characters followed by trailing
58/// zero padding. Leading and trailing ASCII whitespace are trimmed before validation so write-back
59/// verification against the device's padded storage is deterministic.
60/// @param name User-supplied UTF-8 device name.
61/// @param payload Output buffer for the fixed write payload (16 bytes, zero-padded on success).
62/// @param normalized_name Output normalized UTF-8 name used for later verification/logging.
63/// @return Validation status indicating success or the reason the name cannot be written.
65 uint8_t payload[DEVICE_NAME_WRITE_PAYLOAD_SIZE],
66 std::string &normalized_name);
67/// @brief Return a stable symbolic name for a device-name validation result.
68/// @param error Validation result.
69/// @return Uppercase symbolic name.
71/// @brief Return a human-readable explanation for a device-name validation result.
72/// @param error Validation result.
73/// @return Short message suitable for logs and Home Assistant action events.
75
76// ============================================================================
77// Address Classification
78// ============================================================================
79
80/// @brief Well-known address suffix values in the broadcast address space.
81///
82/// When the first byte of a 3-byte IO-Homecontrol address is 0x00, the low 6 bits
83/// of the third byte carry a suffix that identifies the broadcast category.
84/// @{
85static constexpr uint8_t ADDRESS_SUFFIX_MASK = 0x3F; ///< Mask to extract the 6-bit suffix from addr[2].
86static constexpr uint8_t ADDRESS_SUFFIX_BROADCAST = 0x3F; ///< Suffix for "all devices of this type" broadcast.
87static constexpr uint8_t ADDRESS_SUFFIX_DISCOVERY = 0x3B; ///< Suffix for discovery-related broadcasts.
88/// @}
89
90/// @brief Address classification categories for diagnostic purposes.
91///
92/// IO-Homecontrol uses the target address field to encode both unicast
93/// device addresses and broadcast targets. The first byte being 0x00
94/// indicates a broadcast; the remaining bytes encode the target device type.
95enum class AddressClass : uint8_t {
96 UNICAST = 0, ///< Normal device-to-device unicast address (first byte != 0x00).
97 BROADCAST_ALL, ///< Broadcast to all devices of a type (address suffix 0x3F).
98 BROADCAST_TYPE, ///< Broadcast to specific device type with non-standard suffix.
99 DISCOVERY, ///< Discovery-related broadcast (address suffix 0x3B).
100 UNKNOWN_BROADCAST, ///< Broadcast pattern that does not match known suffixes.
101};
102
103/// @brief Classify an IO-Homecontrol 3-byte address.
104///
105/// Determines whether an address is unicast, a typed broadcast, or a
106/// discovery-related broadcast based on protocol addressing rules.
107/// @param addr Three-byte node address to classify.
108/// @return Classification indicating the address type.
109AddressClass classify_address(const uint8_t addr[NODE_ID_SIZE]);
110
111/// @brief Get a human-readable name for an address classification.
112/// @param address_class Classification returned by classify_address().
113/// @return Null-terminated string such as "broadcast_all" or "unicast".
114const char *address_class_name(AddressClass address_class);
115
116/// @brief Extract the target device type from a typed broadcast address.
117///
118/// Broadcast addresses encode the device type in bits [9:2] of the combined
119/// address bytes 1–2. This function extracts that type. Returns UNKNOWN
120/// if the address is not a broadcast (first byte != 0x00).
121/// @param addr Three-byte broadcast address.
122/// @return DeviceType encoded in the address, or DeviceType::UNKNOWN for unicast.
124
125// ============================================================================
126// 1W Remote Frame Decode
127// ============================================================================
128
129/// @brief Decode the "main" position/command bytes from a 1W execute payload.
130///
131/// 1W remotes encode their command intent in a 2-byte main field:
132/// - main[0]: position (0–200 mapped to 0–100%), or a special command code.
133/// - main[1]: modifier byte (0x03 = ventilation for POS_FAVORITE).
134///
135/// @param main0 First main byte (position or special code).
136/// @param main1 Second main byte (modifier).
137/// @param out Buffer to write the decoded string into (e.g., "CLOSE", "position 75%").
138/// @param out_size Size of the output buffer.
139void decode_1w_main_intent(uint8_t main0, uint8_t main1, char *out, size_t out_size);
140
141/// @brief Resolve a 1W main-byte pair to an optimistic IO target position, if unambiguous.
142///
143/// Shares decode_1w_main_intent()'s special-code checks so the two never disagree. Returns
144/// empty for POS_STOP (the caller must clear any optimistic target instead — stop is not a
145/// target) and for codes with no settled position (FAVORITE/VENT/FORCE_OPEN/SECURED_TARGET/
146/// DEFAULT/UNKNOWN) — those cases still get a confirmation poll, just no optimistic claim.
147/// @param main0 First main byte (position or special code).
148/// @param main1 Second main byte (modifier); unused by every branch that resolves a target.
149/// @return IO target position (0=open, 100=closed) if resolvable; empty otherwise.
150std::optional<float> oneway_intent_to_target(uint8_t main0, uint8_t main1);
151
152/// @brief Buffer size for the decoded 1W main-intent string.
153static constexpr size_t ONEWAY_INTENT_BUFFER_SIZE = 24;
154
155/// @brief Decoded representation of a 1W remote frame.
156///
157/// Captures all fields extractable from a 1W broadcast frame in a structured form
158/// that can be used for logging, events, or future sensor exposure.
160 uint8_t src[NODE_ID_SIZE]{}; ///< Remote source node ID (3 bytes).
161 AddressClass address_class{AddressClass::UNKNOWN_BROADCAST}; ///< Classification of the broadcast address.
162 DeviceType target_type{DeviceType::UNKNOWN}; ///< Target device class from broadcast address.
163 uint8_t cmd{0}; ///< Command ID (e.g., CMD_EXECUTE, CMD_ACTIVATE_MODE).
164 bool has_intent{false}; ///< True if originator/ACEI/intent fields were decoded.
165 uint8_t originator{0}; ///< Command originator byte (e.g., ORIGINATOR_USER_REMOTE).
166 uint8_t acei_level{0}; ///< ACEI priority level (0–7).
167 char intent[ONEWAY_INTENT_BUFFER_SIZE]{}; ///< Human-readable command intent (e.g., "CLOSE").
168 uint8_t main0{0}; ///< Raw first main byte (has_intent only); feeds oneway_intent_to_target().
169 uint8_t main1{0}; ///< Raw second main byte (has_intent only); feeds oneway_intent_to_target().
170 uint8_t data_len{0}; ///< Raw data length (for commands without decoded intent).
171};
172
173/// @brief Decode a parsed 1W frame into a structured OneWayFrameInfo.
174///
175/// Extracts target device type from the broadcast address. For execute/activate-mode
176/// commands, also decodes originator, ACEI priority, and position/command intent.
177/// @param frame Parsed IoFrame with CTRL0_PROTOCOL_1W set.
178/// @return Populated OneWayFrameInfo.
179OneWayFrameInfo decode_1w_frame(const IoFrame &frame);
180
181// ============================================================================
182// Discovery Response Decode
183// ============================================================================
184
185/// @brief Extended discovery-response fields (manufacturer, Multi Information Byte, backbone
186/// address, timestamp) plus flags recording how much of the payload was actually present.
188 bool metadata_complete{false}; ///< data_len >= DEVICE_METADATA_SIZE (type/subtype present).
189 bool has_extended{false}; ///< data_len >= DISCOVERY_RESP_FULL_SIZE (mfr/flags/timestamp present).
190 uint8_t manufacturer{0}; ///< Raw manufacturer ID; name via manufacturer_name().
191 uint8_t flags{0}; ///< Multi Information Byte; decode with DISCOVERY_FLAGS_* masks.
192 uint8_t backbone[NODE_ID_SIZE]{}; ///< Backbone address as reported by the device.
193 uint16_t timestamp{0}; ///< Device timestamp field (advances between replies).
194};
195
196/// @brief Decode a discovery-response payload (CMD_DISCOVER_RESP 0x29 or CMD_DISCOVER_SPE_RESP 0x2B —
197/// both carry the identical DISCOVERY_RESP_FULL_SIZE layout) into device metadata.
198///
199/// Pure: no logging, no side effects. Extended fields (manufacturer, flags, timestamp) are
200/// returned rather than logged so each caller can present them its own way. Sets
201/// `device.node_id`/`type`/`subtype`/`inverted`/`position`/`target`/`is_stopped` and
202/// `device_id` exactly as a discovery reply implies: type/subtype and inversion come from the
203/// packed metadata bytes when present, position/target default to the unknown sentinel, and
204/// the device is assumed stopped. Every field read is guarded on `frame.data_len`, so a short
205/// or truncated payload degrades gracefully instead of reading past the end.
206/// @param frame Parsed discovery-response frame.
207/// @param device Output: device record populated from the frame.
208/// @param device_id Output: hex device ID string derived from `frame.src`.
209/// @return Extended discovery fields (manufacturer/flags/timestamp) and length flags.
210DiscoveryResponseInfo decode_discovery_response(const IoFrame &frame, IoDevice &device, std::string &device_id);
211
212} // namespace home_io_control
213} // namespace esphome
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
Definition proto_sizes.h:20
DeviceType
Device type identifiers reported by IO‑Homecontrol products.
@ UNKNOWN
Unknown/unspecified device.
static constexpr uint16_t LATIN1_CODEPOINT_MAX
Highest Unicode code point representable in Latin-1.
static constexpr uint8_t ADDRESS_SUFFIX_BROADCAST
Suffix for "all devices of this type" broadcast.
std::optional< float > oneway_intent_to_target(uint8_t main0, uint8_t main1)
Resolve a 1W main-byte pair to an optimistic IO target position, if unambiguous.
static constexpr uint8_t ADDRESS_SUFFIX_DISCOVERY
Suffix for discovery-related broadcasts.
static constexpr uint8_t ADDRESS_SUFFIX_MASK
Well-known address suffix values in the broadcast address space.
OneWayFrameInfo decode_1w_frame(const IoFrame &frame)
Decode a parsed 1W frame into a structured OneWayFrameInfo.
static constexpr uint8_t DEVICE_NAME_WRITE_PAYLOAD_SIZE
Fixed write payload: 15 visible chars plus trailing null/padding.
const char * device_name_validation_error_name(DeviceNameValidationError error)
Return a stable symbolic name for a device-name validation result.
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.
std::string trim_ascii_whitespace(const std::string &value)
Trim leading and trailing ASCII whitespace from a string.
static constexpr uint8_t DEVICE_NAME_WRITE_CHAR_LIMIT
Reference write limit before the trailing null.
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.
AddressClass classify_address(const uint8_t addr[NODE_ID_SIZE])
Classify an IO-Homecontrol 3-byte address.
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.
DeviceType broadcast_target_type(const uint8_t addr[NODE_ID_SIZE])
Extract the target device type from a typed broadcast address.
AddressClass
Address classification categories for diagnostic purposes.
@ UNKNOWN_BROADCAST
Broadcast pattern that does not match known suffixes.
@ UNICAST
Normal device-to-device unicast address (first byte != 0x00).
@ BROADCAST_TYPE
Broadcast to specific device type with non-standard suffix.
@ BROADCAST_ALL
Broadcast to all devices of a type (address suffix 0x3F).
@ DISCOVERY
Discovery-related broadcast (address suffix 0x3B).
const char * address_class_name(AddressClass address_class)
Get a human-readable name for an address classification.
static constexpr size_t ONEWAY_INTENT_BUFFER_SIZE
Buffer size for the decoded 1W main-intent string.
DiscoveryResponseInfo decode_discovery_response(const IoFrame &frame, IoDevice &device, std::string &device_id)
Decode a discovery-response payload (CMD_DISCOVER_RESP 0x29 or CMD_DISCOVER_SPE_RESP 0x2B — both carr...
const char * device_name_validation_error_description(DeviceNameValidationError error)
Return a human-readable explanation for a device-name validation result.
@ NONE
target_fw is unknown, or its required bootloader matches bootloader_version.
void decode_1w_main_intent(uint8_t main0, uint8_t main1, char *out, size_t out_size)
Decode the "main" position/command bytes from a 1W execute payload.
IO-Homecontrol device-type model, capabilities and runtime device state.
Fundamental IO-Homecontrol frame and crypto size constants.
Extended discovery-response fields (manufacturer, Multi Information Byte, backbone address,...
bool has_extended
data_len >= DISCOVERY_RESP_FULL_SIZE (mfr/flags/timestamp present).
uint8_t backbone[NODE_ID_SIZE]
Backbone address as reported by the device.
bool metadata_complete
data_len >= DEVICE_METADATA_SIZE (type/subtype present).
uint8_t manufacturer
Raw manufacturer ID; name via manufacturer_name().
uint8_t flags
Multi Information Byte; decode with DISCOVERY_FLAGS_* masks.
uint16_t timestamp
Device timestamp field (advances between replies).
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:71
Decoded representation of a 1W remote frame.
bool has_intent
True if originator/ACEI/intent fields were decoded.
uint8_t originator
Command originator byte (e.g., ORIGINATOR_USER_REMOTE).
DeviceType target_type
Target device class from broadcast address.
uint8_t main0
Raw first main byte (has_intent only); feeds oneway_intent_to_target().
uint8_t acei_level
ACEI priority level (0–7).
uint8_t main1
Raw second main byte (has_intent only); feeds oneway_intent_to_target().
uint8_t cmd
Command ID (e.g., CMD_EXECUTE, CMD_ACTIVATE_MODE).
AddressClass address_class
Classification of the broadcast address.
char intent[ONEWAY_INTENT_BUFFER_SIZE]
Human-readable command intent (e.g., "CLOSE").
uint8_t src[NODE_ID_SIZE]
Remote source node ID (3 bytes).
uint8_t data_len
Raw data length (for commands without decoded intent).