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_constants.h"
12#include "proto_device_model.h"
13#include "proto_sizes.h"
14
15#include <cstddef>
16#include <cstdint>
17#include <optional>
18#include <string>
19
20namespace esphome {
21namespace home_io_control {
22
23struct IoFrame; // Defined in proto_frame.h; decode_1w_frame takes it by reference.
24
25// ============================================================================
26// Device Name Codec
27// ============================================================================
28
29static constexpr uint8_t DEVICE_NAME_WRITE_CHAR_LIMIT = 15; ///< Reference write limit before the trailing null.
30static constexpr uint8_t DEVICE_NAME_WRITE_PAYLOAD_SIZE =
31 DEVICE_NAME_WRITE_CHAR_LIMIT + 1; ///< Fixed write payload: 15 visible chars plus trailing null/padding.
32static constexpr uint16_t LATIN1_CODEPOINT_MAX = 0x00FF; ///< Highest Unicode code point representable in Latin-1.
33
34/// @brief Validation result for outbound device-name writes.
35enum class DeviceNameValidationError : uint8_t {
36 NONE = 0x00, ///< Name is valid and encodable.
37 EMPTY = 0x01, ///< Name is empty after normalization.
38 TOO_LONG = 0x02, ///< Name exceeds the 15-character write limit.
39 INVALID_UTF8 = 0x03, ///< Name contains malformed UTF-8 bytes.
40 UNSUPPORTED_CHAR = 0x04, ///< Name contains characters outside Latin-1.
41};
42
43/// @brief Decode a device-name payload from IO-homecontrol's Latin-1 wire format into UTF-8.
44/// Some devices prepend an extra byte before the first character and many pad the payload with
45/// trailing 0x00 or 0x20 bytes. This helper normalizes those quirks and truncates the result to
46/// fit DEVICE_NAME_BUFFER_SIZE - 1 bytes when copied into IoDevice::name.
47/// @param data Raw payload pointer from CMD_GET_NAME_RESP.
48/// @param len Raw payload length in bytes.
49/// @return Normalized UTF-8 name, or an empty string when the payload carries no usable characters.
50std::string decode_device_name_payload(const uint8_t *data, uint8_t len);
51/// @brief Trim leading and trailing ASCII whitespace from a string.
52/// This is shared by device-name validation and management actions so both paths normalize input
53/// consistently before comparing or writing device metadata.
54/// @param value Input string.
55/// @return Copy of the string with leading and trailing ASCII whitespace removed.
56std::string trim_ascii_whitespace(const std::string &value);
57/// @brief Validate and encode a user-supplied UTF-8 device name into the fixed Latin-1 write payload.
58/// The outbound payload is a fixed 16-byte field: up to 15 Latin-1 characters followed by trailing
59/// zero padding. Leading and trailing ASCII whitespace are trimmed before validation so write-back
60/// verification against the device's padded storage is deterministic.
61/// @param name User-supplied UTF-8 device name.
62/// @param payload Output buffer for the fixed write payload (16 bytes, zero-padded on success).
63/// @param normalized_name Output normalized UTF-8 name used for later verification/logging.
64/// @return Validation status indicating success or the reason the name cannot be written.
66 uint8_t payload[DEVICE_NAME_WRITE_PAYLOAD_SIZE],
67 std::string &normalized_name);
68/// @brief Return a stable symbolic name for a device-name validation result.
69/// @param error Validation result.
70/// @return Uppercase symbolic name.
72/// @brief Return a human-readable explanation for a device-name validation result.
73/// @param error Validation result.
74/// @return Short message suitable for logs and Home Assistant action events.
76
77// ============================================================================
78// Address Classification
79// ============================================================================
80
81/// @brief Well-known address suffix values in the broadcast address space.
82///
83/// When the first byte of a 3-byte IO-Homecontrol address is 0x00, the low 6 bits
84/// of the third byte carry a suffix that identifies the broadcast category.
85/// @{
86static constexpr uint8_t ADDRESS_SUFFIX_MASK = 0x3F; ///< Mask to extract the 6-bit suffix from addr[2].
87static constexpr uint8_t ADDRESS_SUFFIX_BROADCAST = 0x3F; ///< Suffix for "all devices of this type" broadcast.
88static constexpr uint8_t ADDRESS_SUFFIX_DISCOVERY = 0x3B; ///< Suffix for discovery-related broadcasts.
89/// @}
90
91/// @brief Address classification categories for diagnostic purposes.
92///
93/// IO-Homecontrol uses the target address field to encode both unicast
94/// device addresses and broadcast targets. The first byte being 0x00
95/// indicates a broadcast; the remaining bytes encode the target device type.
96enum class AddressClass : uint8_t {
97 UNICAST = 0, ///< Normal device-to-device unicast address (first byte != 0x00).
98 BROADCAST_ALL, ///< Broadcast to all devices of a type (address suffix 0x3F).
99 BROADCAST_TYPE, ///< Broadcast to specific device type with non-standard suffix.
100 DISCOVERY, ///< Discovery-related broadcast (address suffix 0x3B).
101 UNKNOWN_BROADCAST, ///< Broadcast pattern that does not match known suffixes.
102};
103
104/// @brief Classify an IO-Homecontrol 3-byte address.
105///
106/// Determines whether an address is unicast, a typed broadcast, or a
107/// discovery-related broadcast based on protocol addressing rules.
108/// @param addr Three-byte node address to classify.
109/// @return Classification indicating the address type.
110AddressClass classify_address(const uint8_t addr[NODE_ID_SIZE]);
111
112/// @brief Get a human-readable name for an address classification.
113/// @param address_class Classification returned by classify_address().
114/// @return Null-terminated string such as "broadcast_all" or "unicast".
115const char *address_class_name(AddressClass address_class);
116
117/// @brief Extract the target device type from a typed broadcast address.
118///
119/// Broadcast addresses encode the device type in bits [9:2] of the combined
120/// address bytes 1–2. This function extracts that type. Returns UNKNOWN
121/// if the address is not a broadcast (first byte != 0x00).
122/// @param addr Three-byte broadcast address.
123/// @return DeviceType encoded in the address, or DeviceType::UNKNOWN for unicast.
124DeviceType broadcast_target_type(const uint8_t addr[NODE_ID_SIZE]);
125
126/// @brief Encode a device type into its typed-broadcast destination address — the exact inverse
127/// of broadcast_target_type(), kept immediately beside it so the pair cannot drift apart.
128///
129/// The class occupies bits [9:2] and therefore **spans two bytes**: `out[0]` is always 0,
130/// `out[1] = raw >> DEVICE_TYPE_LOW_BITS_SHIFT` carries the high bits, and
131/// `out[2] = (raw << DEVICE_TYPE_HIGH_BITS_SHIFT) | DEVICE_SUBTYPE_MASK` carries the low bits
132/// plus the all-ones subtype field that makes the address a broadcast ("any subtype of this
133/// class"). A single-byte encoding looks right for classes 0–3 and silently produces the wrong
134/// class from 4 upward — e.g. the light class (0x06) encodes to `00 01 BF`, not `00 00 BF`.
135/// This is the addressing primitive 1W execute frames use: 1W commands a device *class*, never
136/// an individual node, so this is the only destination a 1W builder ever needs.
137/// @param type Device class to address.
138/// @param out Output: 3-byte typed-broadcast destination address.
139void encode_broadcast_address(DeviceType type, uint8_t out[NODE_ID_SIZE]);
140
141// ============================================================================
142// 1W Remote Frame Decode
143// ============================================================================
144
145/// @brief Decode the "main" position/command bytes from a 1W execute payload.
146///
147/// 1W remotes encode their command intent in a 2-byte main field:
148/// - main[0]: position (0–200 mapped to 0–100%), or a special command code.
149/// - main[1]: modifier byte (0x03 = ventilation for POS_FAVORITE).
150///
151/// @param main0 First main byte (position or special code).
152/// @param main1 Second main byte (modifier).
153/// @param out Buffer to write the decoded string into (e.g., "CLOSE", "position 75%").
154/// @param out_size Size of the output buffer.
155void decode_1w_main_intent(uint8_t main0, uint8_t main1, char *out, size_t out_size);
156
157/// @brief Resolve a 1W main-byte pair to an optimistic IO target position, if unambiguous.
158///
159/// Shares decode_1w_main_intent()'s special-code checks so the two never disagree. Returns
160/// empty for POS_STOP (the caller must clear any optimistic target instead — stop is not a
161/// target) and for codes with no settled position (FAVORITE/VENT/FORCE_OPEN/SECURED_TARGET/
162/// DEFAULT/UNKNOWN) — those cases still get a confirmation poll, just no optimistic claim.
163/// @param main0 First main byte (position or special code).
164/// @param main1 Second main byte (modifier); unused by every branch that resolves a target.
165/// @return IO target position (0=open, 100=closed) if resolvable; empty otherwise.
166std::optional<float> oneway_intent_to_target(uint8_t main0, uint8_t main1);
167
168/// @brief Buffer size for the decoded 1W main-intent string.
169static constexpr size_t ONEWAY_INTENT_BUFFER_SIZE = 24;
170
171/// @brief Decoded representation of a 1W remote frame.
172///
173/// Captures all fields extractable from a 1W broadcast frame in a structured form
174/// that can be used for logging, events, or future sensor exposure.
176 uint8_t src[NODE_ID_SIZE]{}; ///< Remote source node ID (3 bytes).
177 AddressClass address_class{AddressClass::UNKNOWN_BROADCAST}; ///< Classification of the broadcast address.
178 DeviceType target_type{DeviceType::UNKNOWN}; ///< Target device class from broadcast address.
179 uint8_t cmd{0}; ///< Command ID (e.g., CMD_EXECUTE, CMD_ACTIVATE_MODE).
180 bool has_intent{false}; ///< True if originator/ACEI/intent fields were decoded.
181 uint8_t originator{0}; ///< Command originator byte (e.g., ORIGINATOR_USER_REMOTE).
182 uint8_t acei_level{0}; ///< ACEI priority level (0–7).
183 char intent[ONEWAY_INTENT_BUFFER_SIZE]{}; ///< Human-readable command intent (e.g., "CLOSE").
184 uint8_t main0{0}; ///< Raw first main byte (has_intent only); feeds oneway_intent_to_target().
185 uint8_t main1{0}; ///< Raw second main byte (has_intent only); feeds oneway_intent_to_target().
186 uint8_t data_len{0}; ///< Raw data length (for commands without decoded intent).
187};
188
189/// @brief Decode a parsed 1W frame into a structured OneWayFrameInfo.
190///
191/// Extracts target device type from the broadcast address. For execute/activate-mode
192/// commands, also decodes originator, ACEI priority, and position/command intent.
193/// @param frame Parsed IoFrame with CTRL0_PROTOCOL_1W set.
194/// @return Populated OneWayFrameInfo.
195OneWayFrameInfo decode_1w_frame(const IoFrame &frame);
196
197/// @brief The destination label a decoded 1W frame renders as in a log line.
198///
199/// Shared by the RX path (hub_internal.h's `log_1w_remote_frame()`) and the TX path
200/// (`OneWayTransmitter::send_burst()`) so the two cannot drift into naming the same address
201/// differently. The all-devices broadcast decodes to the sentinel `DeviceType::UNKNOWN`, whose
202/// bare `device_type_name()` would render as `unknown`; this special-cases it to `all` instead.
203/// @param info Already-decoded frame info (see decode_1w_frame()).
204/// @return `"all"` for the all-devices broadcast, else the target class's name.
205const char *oneway_target_label(const OneWayFrameInfo &info);
206
207// ============================================================================
208// Discovery Response Decode
209// ============================================================================
210
211/// @brief Extended discovery-response fields (manufacturer, Multi Information Byte, backbone
212/// address, timestamp) plus flags recording how much of the payload was actually present.
214 bool metadata_complete{false}; ///< data_len >= DEVICE_METADATA_SIZE (type/subtype present).
215 bool has_extended{false}; ///< data_len >= DISCOVERY_RESP_FULL_SIZE (mfr/flags/timestamp present).
216 uint8_t manufacturer{0}; ///< Raw manufacturer ID; name via manufacturer_name().
217 uint8_t flags{0}; ///< Multi Information Byte; decode with DISCOVERY_FLAGS_* masks.
218 uint8_t backbone[NODE_ID_SIZE]{}; ///< Backbone address as reported by the device.
219 uint16_t timestamp{0}; ///< Device timestamp field (advances between replies).
220};
221
222/// @brief Decode a discovery-response payload (CMD_DISCOVER_RESP 0x29 or CMD_DISCOVER_SPE_RESP 0x2B —
223/// both carry the identical DISCOVERY_RESP_FULL_SIZE layout) into device metadata.
224///
225/// Pure: no logging, no side effects. Extended fields (manufacturer, flags, timestamp) are
226/// returned rather than logged so each caller can present them its own way. Sets
227/// `device.node_id`/`type`/`subtype`/`inverted`/`position`/`target`/`is_stopped` and
228/// `device_id` exactly as a discovery reply implies: type/subtype and inversion come from the
229/// packed metadata bytes when present, position/target default to the unknown sentinel, and
230/// the device is assumed stopped. Every field read is guarded on `frame.data_len`, so a short
231/// or truncated payload degrades gracefully instead of reading past the end.
232/// @param frame Parsed discovery-response frame.
233/// @param device Output: device record populated from the frame.
234/// @param device_id Output: hex device ID string derived from `frame.src`.
235/// @return Extended discovery fields (manufacturer/flags/timestamp) and length flags.
236DiscoveryResponseInfo decode_discovery_response(const IoFrame &frame, IoDevice &device, std::string &device_id);
237
238// ============================================================================
239// 1W Add-Controller Key Adoption (CMD 0x30)
240// ============================================================================
241
242/// @brief Outcome of checking the out-of-length MAC trailer (`IoFrame::has_mac`) on a decoded
243/// CMD_ONEWAY_ADD_CONTROLLER frame.
244///
245/// Kept distinct from a bare bool because a caller-facing report needs to tell these three
246/// situations apart: no MAC was ever present to check (the reference implementation's own
247/// `_p0x30` struct omits the MAC field entirely, so this is a normal, non-error outcome — not
248/// every 0x30 on the wire carries one), a MAC was present and matched, or a MAC was present and
249/// did not match (evidence the decrypted key is wrong, or the frame was corrupted/forged).
250enum class OneWayMacStatus : uint8_t {
251 NOT_PRESENT = 0, ///< `frame.has_mac` was false; nothing to verify.
252 VERIFIED = 1, ///< `frame.has_mac` was true and the MAC verified under the recovered key.
253 FAILED = 2, ///< `frame.has_mac` was true and the MAC did NOT verify under the recovered key.
254};
255
256/// @brief Recovered controller identity from a decoded CMD_ONEWAY_ADD_CONTROLLER (0x30) frame.
257///
258/// Fixed-size, no heap, no `std::string`, no vectors. Deliberately carries no logging or
259/// `to_string()` helper: `system_key` is a real 1W installation's network key, and the surest
260/// way to leak a key is to make it convenient to print. Route `system_key` through the single
261/// intentional user-facing "adopted key" emission (a later step) and nowhere else — see
262/// `crypto::crypt_1w_key()`'s `@warning` in proto_crypto.h.
263/// @warning Carries real key material (`system_key`). Never log, print, or otherwise let this
264/// struct's contents reach any path other than the one intentional emission.
266 uint8_t system_key[AES_KEY_SIZE]{}; ///< Recovered network system key (crypto::crypt_1w_key() output).
267 uint8_t manufacturer{0}; ///< Manufacturer ID byte from the payload (`man_id`).
268 uint8_t sender_node[NODE_ID_SIZE]{}; ///< Sender's node address (`frame.src`) — the new identity's node.
269 uint16_t sequence{0}; ///< 2-byte rolling sequence from the payload (big-endian on wire).
270 OneWayMacStatus mac_status{OneWayMacStatus::NOT_PRESENT}; ///< MAC-verification outcome; see OneWayMacStatus.
271};
272
273/// @brief Decoding outcome for decode_1w_add_controller().
274enum class OneWayAddControllerDecodeError : uint8_t {
275 NONE = 0, ///< Decode succeeded; `out` is populated.
276 NOT_ONEWAY = 1, ///< `CTRL0_PROTOCOL_1W` is not set — not a 1W frame at all.
277 WRONG_COMMAND = 2, ///< `frame.cmd` is not CMD_ONEWAY_ADD_CONTROLLER.
278 BAD_LENGTH = 3, ///< Declared payload is not exactly the expected 20 bytes.
279 KEY_UNWRAP_FAILED = 4, ///< crypto::crypt_1w_key() itself reported failure.
280};
281
282/// @brief Decode a CMD_ONEWAY_ADD_CONTROLLER (0x30) frame into a recovered controller identity.
283///
284/// A 1W device broadcasts this frame while its key-copy gesture is active, handing its network's
285/// wrapped system key to whichever controller is listening (see CMD_ONEWAY_ADD_CONTROLLER's
286/// Doxygen in proto_constants.h). Three facts a future reader cannot re-derive from the code
287/// alone:
288/// - The AES key that wraps the payload is the public TRANSFER_KEY (proto_constants.h), not
289/// the recovered system key — the system key is the *output* of unwrapping, not an input.
290/// - The unwrap IV is derived only from the sender's own node address (`frame.src`), which is
291/// plaintext in the frame header — no secret or challenge is needed to begin unwrapping.
292/// - The wrap is self-inverse (crypto::crypt_1w_key()'s XOR-with-AES-keystream construction),
293/// so the same call that would encrypt a plaintext key for transmission also decrypts an
294/// overheard ciphertext; there is no direction flag to get wrong.
295///
296/// Validates before decrypting rather than decrypting garbage: the 1W protocol bit must be set,
297/// the command must be CMD_ONEWAY_ADD_CONTROLLER, and the declared payload must be exactly the
298/// expected 20 bytes (`enc_key[16] + man_id[1] + data[1] + sequence[2]`) — any mismatch is
299/// rejected before `crypto::crypt_1w_key()` is ever called.
300///
301/// When `frame.has_mac` is true (the out-of-length MAC trailer modeled by `IoFrame::mac`, see
302/// proto_frame.h), the MAC is verified under the *recovered* key with span `cmd + enc_key` (17
303/// bytes) — the only span CMD_ONEWAY_ADD_CONTROLLER authenticates (see
304/// `crypto::create_1w_hmac()`'s `@warning`). A verifying MAC is strong evidence the decryption
305/// recovered the correct key: it is the decisive self-check available at adoption time, before
306/// any device is ever commanded. A frame with no MAC is not an error — the reference
307/// implementation's own `_p0x30` struct omits the MAC field entirely — so decoding still
308/// succeeds and `out.mac_status` simply reports `NOT_PRESENT`.
309///
310/// Pure function of the frame: no logging, no timers, no I/O (ADR 0005). Never transmits.
311/// @param frame Parsed IoFrame, expected to be a CMD_ONEWAY_ADD_CONTROLLER 1W frame.
312/// @param out Output: recovered controller identity. Only meaningfully populated when the return
313/// value is NONE; left default-constructed (zeroed) otherwise.
314/// @return NONE on success; otherwise the specific validation/decode failure.
315/// @warning `out` carries real key material once populated. See OneWayAdoptedKey's warning.
316OneWayAddControllerDecodeError decode_1w_add_controller(const IoFrame &frame, OneWayAdoptedKey &out);
317
318// ============================================================================
319// Last-command record (CMD_PRIVATE_RESP / CMD_STATUS_UPDATE)
320// ============================================================================
321
322/// @brief Offset of the last-command record within each status-bearing payload.
323///
324/// Both status-bearing frame types carry the same 4-byte record — three bytes of node ID for the
325/// controller that last commanded the device, then that command's Command Originator byte — and
326/// 0x71's whole payload is shifted +3 relative to 0x04's, exactly as its target/current position
327/// fields are. Confirmed on one device in one session across both frame types:
328/// tests/corpus/captures/statuspoll/somfy_rs100_statuspoll_kig300_sx1276.yaml, device E461E9,
329/// which names controller BE FE DB at 0x04 data[8..10] and 0x71 data[11..13] in the same capture —
330/// including in a 0x71 addressed to a *different* controller, which is what rules out "this is
331/// just the destination echoed back".
332inline constexpr uint8_t PRIVATE_RESPONSE_LAST_COMMAND_OFFSET = 8;
333inline constexpr uint8_t STATUS_UPDATE_LAST_COMMAND_OFFSET = 11;
334
335/// @brief Offset of the Command Originator byte in a CMD_STATUS_UPDATE (0x71) payload.
336///
337/// Deliberately not offset 1: `data[1]` on a 0x71 is the status byte (0x60/0x61, bit 0 = current
338/// position unknown), which matches no ORIGINATOR_* value, so reading it as an originator rendered
339/// "unknown" on every frame this project has ever captured. Every captured 0x71 carries 0x01
340/// (ORIGINATOR_USER_REMOTE) here.
341inline constexpr uint8_t STATUS_UPDATE_ORIGINATOR_OFFSET = 14;
343 "the Command Originator byte is the fourth byte of the last-command record; if one "
344 "offset moves the other must move with it");
345
346/// @brief One decoded last-command record.
348 uint8_t commander[NODE_ID_SIZE]{}; ///< Controller that last commanded the device.
349 uint8_t originator{0}; ///< That command's Command Originator (ORIGINATOR_*).
350 bool valid{false}; ///< False when the payload was too short, or the record was unpopulated.
351};
352
353/// @brief Decode the last-command record at `base` from a status-bearing payload.
354///
355/// Pure, so it is testable against corpus bytes directly. An all-zero commander is reported as invalid: 00 00 00 is not
356/// a node ID any observed controller uses, so a device that pads this field rather than implementing it publishes
357/// nothing instead of a fabricated address.
358/// @param frame A CMD_PRIVATE_RESP or CMD_STATUS_UPDATE frame.
359/// @param base PRIVATE_RESPONSE_LAST_COMMAND_OFFSET or STATUS_UPDATE_LAST_COMMAND_OFFSET.
360/// @return The decoded record, or `valid == false`.
361LastCommandRecord decode_last_command_record(const IoFrame &frame, uint8_t base);
362
363// ============================================================================
364// Limitation read (0x25 / 0x26)
365// ============================================================================
366
367/// @brief One decoded CMD_LIMITATION_STATUS_RESP payload.
368///
369/// The value layout is confirmed: it fits the replies of a VELUX window dry (0 % minimum, 100 %
370/// maximum) and under rain (93 % minimum), and the limitation status of the KLF 200 API
371/// specification v3.18, section 10.5.4 (issue #98). The originator and remaining-time bytes read
372/// 00 in every reply captured so far, rain included, so their meaning is still an assumption.
373/// A Somfy awning answers with another value in the first byte, which decodes as a non-main
374/// parameter.
376 uint8_t parameter_id{0}; ///< data[0]; LIMITATION_PARAM_MP for the main parameter.
377 uint16_t value_raw{0}; ///< data[1..2], big-endian, position coding (STATUS_POS_MAX = 100 %).
378 uint8_t originator{0}; ///< data[3], Command Originator that imposed the limit (ORIGINATOR_*).
379 uint8_t time_raw{0}; ///< data[4], remaining-time code; see limitation_time_seconds().
380};
381
382/// @brief Decode a limitation reply (assumed layout, see LimitationStatus).
383/// @param f A parsed frame.
384/// @param out Receives the decoded fields on success.
385/// @return false unless `f` is a CMD_LIMITATION_STATUS_RESP carrying exactly LIMITATION_RESP_DATA_SIZE bytes.
386bool decode_limitation_status(const IoFrame &f, LimitationStatus &out);
387
388/// @brief Decode a limitation request (assumed layout `[selector] [parameter id] [00]`).
389/// @param f A parsed frame.
390/// @param type Receives the limit the request asks for.
391/// @param parameter_id Receives the parameter id (data[1]).
392/// @return false unless `f` is a CMD_LIMITATION_STATUS_REQ carrying exactly LIMITATION_REQ_DATA_SIZE
393/// bytes with one of the two known selectors.
394bool decode_limitation_request(const IoFrame &f, LimitationType &type, uint8_t &parameter_id);
395
396/// @brief Remaining time of a limitation in seconds.
397/// @param time_raw Time code (LimitationStatus::time_raw).
398/// @return `(n + 1) * 30` for codes 0..252, UINT32_MAX for LIMITATION_TIME_UNLIMITED, and 0 for the
399/// timer-delete codes 254/255, which are not durations.
400uint32_t limitation_time_seconds(uint8_t time_raw);
401
402} // namespace home_io_control
403} // namespace esphome
bool decode_limitation_status(const IoFrame &f, LimitationStatus &out)
Decode a limitation reply (assumed layout, see LimitationStatus).
OneWayMacStatus
Outcome of checking the out-of-length MAC trailer (IoFrame::has_mac) on a decoded CMD_ONEWAY_ADD_CONT...
@ NOT_PRESENT
frame.has_mac was false; nothing to verify.
void encode_broadcast_address(DeviceType type, uint8_t out[NODE_ID_SIZE])
Encode a device type into its typed-broadcast destination address — the exact inverse of broadcast_ta...
uint32_t limitation_time_seconds(uint8_t time_raw)
Remaining time of a limitation in seconds.
OneWayAddControllerDecodeError decode_1w_add_controller(const IoFrame &frame, OneWayAdoptedKey &out)
Decode a CMD_ONEWAY_ADD_CONTROLLER (0x30) frame into a recovered controller identity.
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.
constexpr uint8_t STATUS_UPDATE_LAST_COMMAND_OFFSET
static constexpr uint8_t ADDRESS_SUFFIX_DISCOVERY
Suffix for discovery-related broadcasts.
const char * oneway_target_label(const OneWayFrameInfo &info)
The destination label a decoded 1W frame renders as in a log line.
bool decode_limitation_request(const IoFrame &f, LimitationType &type, uint8_t &parameter_id)
Decode a limitation request (assumed layout [selector] [parameter id] [00]).
DeviceType
IO-Homecontrol node types.
@ UNKNOWN
Unknown/unspecified device.
constexpr uint8_t STATUS_UPDATE_ORIGINATOR_OFFSET
Offset of the Command Originator byte in a CMD_STATUS_UPDATE (0x71) payload.
LastCommandRecord decode_last_command_record(const IoFrame &frame, uint8_t base)
Decode the last-command record at base from a status-bearing payload.
OneWayAddControllerDecodeError
Decoding outcome for decode_1w_add_controller().
@ WRONG_COMMAND
frame.cmd is not CMD_ONEWAY_ADD_CONTROLLER.
@ NOT_ONEWAY
CTRL0_PROTOCOL_1W is not set — not a 1W frame at all.
@ BAD_LENGTH
Declared payload is not exactly the expected 20 bytes.
@ KEY_UNWRAP_FAILED
crypto::crypt_1w_key() itself reported failure.
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.
@ FAILED
No usable reply; the device may never have heard the request.
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.
LimitationType
On-air selector byte (request data[0]) choosing which resulting limit to read.
static constexpr size_t ONEWAY_INTENT_BUFFER_SIZE
Buffer size for the decoded 1W main-intent string.
constexpr uint8_t PRIVATE_RESPONSE_LAST_COMMAND_OFFSET
Offset of the last-command record within each status-bearing payload.
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.
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 command IDs, result codes and protocol enumerations.
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:93
One decoded last-command record.
uint8_t commander[NODE_ID_SIZE]
Controller that last commanded the device.
bool valid
False when the payload was too short, or the record was unpopulated.
uint8_t originator
That command's Command Originator (ORIGINATOR_*).
One decoded CMD_LIMITATION_STATUS_RESP payload.
uint8_t parameter_id
data[0]; LIMITATION_PARAM_MP for the main parameter.
uint8_t originator
data[3], Command Originator that imposed the limit (ORIGINATOR_*).
uint16_t value_raw
data[1..2], big-endian, position coding (STATUS_POS_MAX = 100 %).
uint8_t time_raw
data[4], remaining-time code; see limitation_time_seconds().
Recovered controller identity from a decoded CMD_ONEWAY_ADD_CONTROLLER (0x30) frame.
uint8_t manufacturer
Manufacturer ID byte from the payload (man_id).
OneWayMacStatus mac_status
MAC-verification outcome; see OneWayMacStatus.
uint8_t system_key[AES_KEY_SIZE]
Recovered network system key (crypto::crypt_1w_key() output).
uint8_t sender_node[NODE_ID_SIZE]
Sender's node address (frame.src) — the new identity's node.
uint16_t sequence
2-byte rolling sequence from the payload (big-endian on wire).
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).