Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
hub_internal.h
Go to the documentation of this file.
1#pragma once
2
3/// @file hub_internal.h
4/// @brief Internal helpers shared by the hub implementation .cpp files.
5/// @ingroup hioc_hub
6///
7/// This header is intentionally private to the component implementation. It keeps
8/// small cross-file helpers in one place while leaving hub_core.h focused on the
9/// public component shape and the member-function declarations.
10
11#include "hub_core.h"
12#include "log_frame.h"
13
14#include "esphome/core/log.h"
15
16#include <algorithm>
17#include <array>
18#include <cinttypes>
19#include <cmath>
20#include <cstdio>
21#include <map>
22#include <string>
23#include <vector>
24
25namespace esphome {
26namespace home_io_control {
27namespace detail {
28
29// ============================================================================
30// Shared constants
31// ============================================================================
32
33inline constexpr const char *TAG = "home_io_control"; ///< Shared log tag for hub-level messages.
34/// Suppress a repeated 1W log/poll for the same remote *and the same intent* within this window.
35/// Wide on purpose: it collapses both the 4×/40ms reliability burst and a held button into one
36/// logical press. A *different* intent from the same remote (a stop after a move) is not a
37/// duplicate and passes through immediately — see decisions::is_duplicate_1w_frame().
38inline constexpr uint32_t ONEWAY_DEDUP_WINDOW_MS = 2000;
40 50.0F; ///< Shared 0-100 cutoff: values below this mean binary "on".
41
42// ============================================================================
43// Percent conversion helpers
44// ============================================================================
45
46/// @brief Convert a 0.0-1.0 HA fraction (position, tilt, or brightness) to a 0-100 IO percent.
47///
48/// Rounds rather than truncates: HA quantizes call values to 0-255 before they ever reach us, so
49/// its "50%" is 128/255=0.50196, not exactly 0.5 — a truncating cast compounds that quantization
50/// into a consistent ~1% bias, caught on real hardware in both platform_cover.cpp (position and
51/// tilt) and platform_light.cpp (brightness). Callers apply their own invert/complement logic
52/// (e.g. `1.0F - fraction`) before calling this; it only owns the rounding.
53/// @param fraction Value in [0.0, 1.0].
54/// @return Rounded 0-100 percent.
55inline uint8_t round_percent(float fraction) { return static_cast<uint8_t>(std::lround(fraction * 100.0F)); }
56
57// ============================================================================
58// Capability and entity-profile helpers
59// ============================================================================
60
61/// @brief Is the given position value an on/off binary encoding?
62/// @param position Position value to test.
63/// @return true if position equals BINARY_ENTITY_ON_POSITION or BINARY_ENTITY_OFF_POSITION.
64inline bool is_binary_entity_position(uint8_t position) {
65 return position == BINARY_ENTITY_ON_POSITION || position == BINARY_ENTITY_OFF_POSITION;
66}
67
68/// @brief Does the device's type match the expected HA entity class?
69/// UNKNOWN devices always match to keep imported/discovered devices working.
70/// @param dev IoDevice to check.
71/// @param expected Desired capability class (COVER, LIGHT, SWITCH, etc.).
72/// @return true if device type matches or is UNKNOWN.
74 return dev.type == DeviceType::UNKNOWN || device_capability_class(dev.type) == expected;
75}
76
77/// @brief Does the device support status requests?
78/// UNKNOWN devices pass through.
79/// @param dev IoDevice to check.
80/// @return true if device type supports status requests or is UNKNOWN.
84
85/// @brief Can this device accept an execute (position) command?
86/// Checks capability and, for unknown types, allows binary positions for light/switch.
87/// @param dev IoDevice to check.
88/// @param position Position value being sent.
89/// @return true if operation is appropriate for this device type.
90inline bool known_device_accepts_execute_position(const IoDevice &dev, uint8_t position) {
91 if (dev.type == DeviceType::UNKNOWN)
92 return true;
94 return true;
95 // Dimmable lights (platform_light.cpp's dimmable: true) send arbitrary 0-100 IO positions, not
96 // just the two binary extremes — accept the full range for LIGHT here and trust the entity
97 // layer to only ever send binary values for a non-dimmable light. SWITCH and LOCK have no
98 // continuous concept, so they stay restricted to the binary encoding below.
100 return position <= BINARY_ENTITY_OFF_POSITION;
101 return is_binary_entity_position(position) &&
103}
104
105/// @brief Can this device accept a tilt command?
106/// @param dev IoDevice to check.
107/// @return true only if device type is known to support tilt.
110}
111
112// ============================================================================
113// Logging helpers
114// ============================================================================
115
116/// @brief Log a rejected operation with capability mismatch details.
117/// @param device_id Device ID string.
118/// @param dev IoDevice that rejected the command.
119/// @param operation Human‑readable operation name (e.g., "set position").
120/// @param expected Expected capability class or profile name.
121inline void log_rejected_operation(const std::string &device_id, const IoDevice &dev, const char *operation,
122 const char *expected) {
123 ESP_LOGW(TAG, "Rejecting %s for device %s: type=%s (%u) class=%s profile=%s expected=%s", operation,
124 device_id.c_str(), device_type_name(dev.type), static_cast<uint8_t>(dev.type),
126}
127
128/// @brief Log a frame at the "io_capture" tag with structured fields.
129/// Used for protocol‑level debugging (phases: component, tx, rx, parse_ok/parse_fail).
130/// @param radio Radio driver instance (provides chip name and capture).
131/// @param stage String label for the current phase.
132/// @param buf Raw bytes being logged.
133/// @param len Length of buf.
134/// @param frame Optional parsed IoFrame for decoded fields (cmd, src, dst).
135inline void log_component_capture(const RadioDriver *radio, const char *stage, const uint8_t *buf, uint8_t len,
136 const IoFrame *frame = nullptr) {
137 const RadioCaptureInfo &capture = radio->get_last_capture();
138 char payload_hex[FRAME_LOG_HEX_BUFFER_SIZE];
139 // Masks the 0x32 key-transfer payload exactly like log_frame() (log_frame.h) — this path is
140 // separate from log_frame() and runs on every received frame, including a passively overheard
141 // pairing exchange between two other devices, so it must carry the same redaction guarantee.
142 render_frame_hex_redacted(buf, len, payload_hex, sizeof(payload_hex));
143 if (frame != nullptr) {
144 ESP_LOGD("io_capture",
145 "chip=%s phase=component stage=%s freq=%" PRIu32 " ts=%" PRIu32
146 " len=%u cmd=0x%02X src=%02X%02X%02X dst=%02X%02X%02X payload=%s",
147 radio->chip_name(), stage, capture.freq_hz, capture.timestamp_ms, len, frame->cmd, frame->src[0],
148 frame->src[1], frame->src[2], frame->dst[0], frame->dst[1], frame->dst[2], payload_hex);
149 return;
150 }
151 ESP_LOGD("io_capture", "chip=%s phase=component stage=%s freq=%" PRIu32 " ts=%" PRIu32 " len=%u payload=%s",
152 radio->chip_name(), stage, capture.freq_hz, capture.timestamp_ms, len, payload_hex);
153}
154
155/// @brief Log a frame‑level issue (unregistered endpoints, unsupported commands).
156/// @param component Pointer to the component (for device lookup).
157/// @param direction "tx" or "rx".
158/// @param reason Short issue label (e.g., "unregistered_device").
159/// @param frame Parsed frame.
160/// @param len Serialized length.
161inline void log_frame_issue(IOHomeControlComponent *component, const char *direction, const char *reason,
162 const IoFrame &frame, uint8_t len) {
163 const std::string src_id = node_id_to_string(frame.src);
164 const std::string dst_id = node_id_to_string(frame.dst);
165 const bool src_registered = component->get_device(src_id) != nullptr;
166 const bool dst_registered = component->get_device(dst_id) != nullptr;
167
168 if (src_registered || dst_registered) {
169 ESP_LOGW(TAG, "%s issue=%s cmd=%s(0x%02X) src=%s%s dst=%s%s len=%u data_len=%u", direction, reason,
170 command_name(frame.cmd), frame.cmd, src_id.c_str(), src_registered ? " (registered)" : "", dst_id.c_str(),
171 dst_registered ? " (registered)" : "", len, frame.data_len);
172 return;
173 }
174
175 ESP_LOGD(TAG, "%s issue=%s cmd=%s(0x%02X) src=%s dst=%s len=%u data_len=%u", direction, reason,
176 command_name(frame.cmd), frame.cmd, src_id.c_str(), dst_id.c_str(), len, frame.data_len);
177}
178
179// ============================================================================
180// 1W remote frame decode
181// ============================================================================
182
183/// @brief Log an already-decoded 1W remote frame at DEBUG level.
184///
185/// Formats a concise DEBUG log line showing remote ID, target type, command intent, and
186/// priority. When the remote is linked to devices, appends the linked device IDs. Takes the
187/// already-decoded OneWayFrameInfo so callers that also build a HA event (see
188/// build_sender_event_data()) decode the frame once, not twice.
189/// @param info Already-decoded 1W frame info (see decode_1w_frame()).
190/// @param linked_devices Optional pointer to device IDs this remote is linked to.
191inline void log_1w_remote_frame(const OneWayFrameInfo &info, const std::vector<std::string> *linked_devices = nullptr) {
192 const std::string src_id = node_id_to_string(info.src);
193
194 // Resolve the broadcast target label: "all" for BROADCAST_ALL, otherwise the device type name.
195 const char *target_label =
197
198 // Build optional suffix showing linked devices.
199 std::string suffix;
200 if (linked_devices != nullptr && !linked_devices->empty()) {
201 suffix = " (linked →";
202 for (const auto &dev_id : *linked_devices) {
203 suffix += ' ';
204 suffix += dev_id;
205 }
206 suffix += ')';
207 }
208
209 if (info.has_intent) {
210 ESP_LOGD(TAG, "rx 1W remote %s targets %s: %s(0x%02X) %s originator=%s priority=%s%s", src_id.c_str(), target_label,
211 command_name(info.cmd), info.cmd, info.intent, originator_name(info.originator),
212 acei_level_name(info.acei_level), suffix.c_str());
213 return;
214 }
215
216 ESP_LOGD(TAG, "rx 1W remote %s targets %s: %s(0x%02X) data_len=%u%s", src_id.c_str(), target_label,
217 command_name(info.cmd), info.cmd, info.data_len, suffix.c_str());
218}
219
220/// @brief Home Assistant event fired when a decoded 1W frame carries a command intent from an
221/// exposed sender (a physical remote button press, or a wind/rain sensor's triggered command).
222inline constexpr const char *ONEWAY_SENDER_EVENT = "esphome.home_io_control_sender_event";
223
224/// @brief Whether a 1W sender is on the `exposed_senders` allowlist for the sender HA event.
225///
226/// "Sender" covers both remotes and wind/rain sensors — they use the identical 1W broadcast
227/// mechanism and differ only in the `originator` byte inside the payload, not in addressing.
228/// Overheard 1W traffic is always DEBUG-logged regardless of this check (see
229/// log_1w_remote_frame()); this only gates whether the event reaches Home Assistant. Deliberately
230/// separate from `linked_devices` — a sender can be event-enabled without controlling any
231/// registered device (e.g. to trigger an HA automation with no matching cover/light/switch), or
232/// vice versa.
233/// @param exposed_senders Configured allowlist (`exposed_senders` YAML key, empty by default).
234/// @param sender_id Node ID of the 1W sender that sent the frame.
235/// @return true if the sender is in the allowlist.
236inline bool is_exposed_sender(const std::vector<std::string> &exposed_senders, const std::string &sender_id) {
237 return std::find(exposed_senders.begin(), exposed_senders.end(), sender_id) != exposed_senders.end();
238}
239
240/// Buffer size for format_name_and_hex(): longest command name plus "(0xXX)" and a margin.
241inline constexpr size_t NAME_AND_HEX_BUFFER_SIZE = 40;
242
243/// @brief Format a name/value pair as "name(0xXX)", e.g. "execute(0x00)".
244inline std::string format_name_and_hex(const char *name, uint8_t value) {
245 std::array<char, NAME_AND_HEX_BUFFER_SIZE> buffer{};
246 std::snprintf(buffer.data(), buffer.size(), "%s(0x%02X)", name, value);
247 return std::string(buffer.data());
248}
249
250/// Buffer size for describe_learned_device_type()'s hex fallback: "io_device_type: 0xXX" plus margin.
251inline constexpr size_t LEARNED_DEVICE_TYPE_HEX_BUFFER_SIZE = 24;
252
253/// @brief Build the YAML line to add once a device's type is learned at runtime.
254///
255/// Logged when `io_device_type` was left unset in YAML and an INFO2 response just resolved it
256/// for the first time this boot (ADR 0018: nothing persists, so this repeats on every reboot
257/// until the user copies the line in). Reuses yaml_device_type_name() so the exact syntax always
258/// matches what the pairing snippet and the Python schema accept.
259/// @param type The now-known device type. Must not be DeviceType::UNKNOWN.
260/// @return The YAML line to add, e.g. `io_device_type: "venetian_blind"` or `io_device_type: 0x11`.
261inline std::string describe_learned_device_type(DeviceType type) {
262 const char *name = yaml_device_type_name(type);
263 if (name != nullptr)
264 return std::string("io_device_type: \"") + name + "\"";
265 std::array<char, LEARNED_DEVICE_TYPE_HEX_BUFFER_SIZE> buffer{};
266 std::snprintf(buffer.data(), buffer.size(), "io_device_type: 0x%02X", static_cast<uint8_t>(type));
267 return std::string(buffer.data());
268}
269
270/// @brief Build the Home Assistant event data map for a decoded 1W sender frame.
271///
272/// Only meaningful when `info.has_intent` is true (the caller gates emission on that); the
273/// `intent` field is only populated by decode_1w_frame() in that case.
274/// @param info Already-decoded 1W frame info (see decode_1w_frame()).
275/// @param linked True if this sender is linked to at least one registered device.
276/// @return Event data map ready for fire_homeassistant_event().
277inline std::map<std::string, std::string> build_sender_event_data(const OneWayFrameInfo &info, bool linked) {
278 return {
279 {"remote_id", node_id_to_string(info.src)},
280 {"target_class", address_class_name(info.address_class)},
281 {"target_type", device_type_name(info.target_type)},
282 {"cmd", format_name_and_hex(command_name(info.cmd), info.cmd)},
283 {"intent", info.intent},
284 {"originator", originator_name(info.originator)},
285 {"acei_level", acei_level_name(info.acei_level)},
286 {"linked", linked ? "true" : "false"},
287 };
288}
289
290// ============================================================================
291// Status normalization helpers
292// ============================================================================
293
294/// @brief Normalize stopped state: some devices briefly report stopped before target/current converge.
295/// @param dev Device record to update (may clear is_stopped if positions differ).
297 // Some devices briefly report STATUS_STOPPED before current and target have numerically
298 // converged. Keep the device in the moving state until the decoded values are effectively equal.
299 if (dev.is_stopped && dev.target != UNKNOWN_POSITION && dev.position != UNKNOWN_POSITION &&
301 dev.is_stopped = false;
302 }
303}
304
305/// @brief Update per-device link-health stats from the radio's last capture.
306///
307/// Called for every frame whose `src` is a registered device, regardless of command type or
308/// whether that command's own payload was well-formed — any such frame is real evidence the
309/// device is reachable and at this signal strength. The two call sites cover both ways such a
310/// frame arrives: update_device_status_() (inbound status path) and
311/// execute_request_and_update_()'s explicit-refusal branch (a CMD_ERROR_RESP reply to our own
312/// request, which returns before reaching the status path). Always stamps `last_seen_ms`; only
313/// touches the RSSI fields when `radio` is non-null and its last capture is valid (real drivers
314/// always populate a valid capture before a frame is handed off, but tests calling this path
315/// directly without a radio, or without exercising RX through it, must not crash or fabricate
316/// an RSSI).
317/// @param dev Device that sent the frame.
318/// @param radio Radio driver to read the last capture from; may be nullptr.
319inline void update_link_health(IoDevice &dev, RadioDriver *radio) {
320 dev.last_seen_ms = millis();
321 if (radio == nullptr)
322 return;
323
324 const RadioCaptureInfo &capture = radio->get_last_capture();
325 if (!capture.valid)
326 return;
327
328 dev.last_rssi_dbm = capture.rssi_dbm;
330 // First sample: seed the EMA directly instead of blending from 0.
331 dev.rssi_ema_scaled = static_cast<int16_t>(capture.rssi_dbm * RSSI_EMA_SCALE);
332 return;
333 }
334 // Integer EMA kept in fixed point: `S += x − round(S/N)` blends each sample at weight 1/N
335 // while S stays scaled by N, so sub-dBm contributions accumulate instead of truncating to
336 // zero — a whole-dBm EMA would stall as soon as |sample − EMA| < N and never converge.
337 dev.rssi_ema_scaled =
338 static_cast<int16_t>(dev.rssi_ema_scaled + capture.rssi_dbm - rssi_scaled_to_dbm(dev.rssi_ema_scaled));
339}
340
341/// @brief Record that an outbound exchange to this device timed out (no valid response).
342///
343/// Called once per failed exchange from execute_request_and_update_()'s "no valid response"
344/// branch — the single place every device-directed exchange already reads
345/// ExchangeEngine::DebugInfo. Both counters saturate at UINT16_MAX instead of wrapping,
346/// matching PairingTelemetry's counters.
347/// @param dev Device the failed exchange was addressed to.
348/// @param tries Number of attempts the failed exchange made (`DebugInfo::tries`, 1-based).
349inline void record_exchange_timeout(IoDevice &dev, uint8_t tries) {
350 if (dev.exchange_timeout_count < UINT16_MAX)
353 static_cast<uint16_t>(std::min<uint32_t>(static_cast<uint32_t>(dev.exchange_attempt_count) + tries, UINT16_MAX));
354}
355
356/// @brief Log a concise status‑update line used by inbound handlers.
357/// @param id Device ID.
358/// @param dev Current device state.
359/// @param suffix Optional suffix added after the state string (e.g., " (status update)").
360inline void log_status_update(const std::string &id, const IoDevice &dev, const char *suffix = "") {
361 ESP_LOGI(TAG, "Device %s: position=%s target=%s %s%s", id.c_str(), format_position(dev.position).c_str(),
362 format_position(dev.target).c_str(), dev.is_stopped ? "stopped" : "moving", suffix);
363}
364
365/// @brief Log a decoded CMD_ERROR_RESP result with optional request-command context.
366/// @param id Device ID.
367/// @param result Result byte from CMD_ERROR_RESP data[0].
368/// @param request_cmd Original outbound request command when known.
369/// @param include_request_cmd True to include request_cmd in the log line.
370inline void log_command_result(const std::string &id, uint8_t result, uint8_t request_cmd = 0,
371 bool include_request_cmd = false) {
372 const char *kind = is_limitation_result(result) ? "limitation" : "error";
373 if (include_request_cmd) {
374 ESP_LOGW(TAG, "Device %s: %s (0x%02X) returned %s result=0x%02X %s (%s)", id.c_str(), command_name(request_cmd),
375 request_cmd, kind, result, command_result_name(result), command_result_description(result));
376 return;
377 }
378
379 ESP_LOGW(TAG, "Device %s: explicit %s result=0x%02X %s (%s)", id.c_str(), kind, result, command_result_name(result),
381}
382
383/// @brief Store a decoded CMD_ERROR_RESP result on the device and log it.
384///
385/// Single place both CMD_ERROR_RESP call sites (the unsolicited status path and the reply to
386/// our own EXECUTE) route through, so the store-and-log policy cannot drift between them. Does
387/// not notify subscribers itself — callers already call notify_device_update_() once per
388/// handled frame; call it after this.
389/// @param dev Device that returned the result.
390/// @param id Device ID (for the log line).
391/// @param result Result byte from CMD_ERROR_RESP data[0].
392/// @param request_cmd Original outbound request command when known.
393/// @param include_request_cmd True to include request_cmd context in the log line.
394inline void record_command_result(IoDevice &dev, const std::string &id, uint8_t result, uint8_t request_cmd = 0,
395 bool include_request_cmd = false) {
396 dev.last_result_code = result;
397 dev.last_result_at_ms = millis();
398 log_command_result(id, result, request_cmd, include_request_cmd);
399}
400
401/// @brief Clear a previously recorded CMD_ERROR_RESP result, if any.
402///
403/// A stale limitation reason (e.g. a rain lockout from an hour ago) is worse than none once the
404/// device has since replied normally, so every successful status/command reply for a device
405/// clears it. Called from the CMD_PRIVATE_RESP and CMD_STATUS_UPDATE branches of
406/// update_device_status_() — not from CMD_GET_NAME_RESP/CMD_GET_INFO2_RESP, which are metadata
407/// lookups unrelated to whether the device's last movement command succeeded. Like
408/// record_command_result(), does not notify subscribers itself — both existing call sites clear
409/// before their own notify_device_update_() call, which is what actually publishes this change.
410/// @param dev Device to clear.
412 dev.last_result_code = 0;
413 dev.last_result_at_ms = 0;
414}
415
416} // namespace detail
417} // namespace home_io_control
418} // namespace esphome
The main IO-Homecontrol component.
Definition hub_core.h:74
virtual IoDevice * get_device(const std::string &device_id)
Retrieve a device by ID; returns nullptr if not found.
Definition hub_core.cpp:304
Abstract radio driver for IO-Homecontrol.
const RadioCaptureInfo & get_last_capture() const
Get the most recent radio capture info.
virtual const char * chip_name() const =0
Get a human‑readable chip name.
IO-Homecontrol ESPHome component — protocol controller.
Shared frame logging helpers for IO-Homecontrol.
bool known_device_accepts_execute_tilt(const IoDevice &dev)
Can this device accept a tilt command?
bool known_device_matches_entity_class(const IoDevice &dev, DeviceCapabilityClass expected)
Does the device's type match the expected HA entity class?
void log_rejected_operation(const std::string &device_id, const IoDevice &dev, const char *operation, const char *expected)
Log a rejected operation with capability mismatch details.
bool known_device_accepts_execute_position(const IoDevice &dev, uint8_t position)
Can this device accept an execute (position) command?
std::string format_name_and_hex(const char *name, uint8_t value)
Format a name/value pair as "name(0xXX)", e.g. "execute(0x00)".
std::map< std::string, std::string > build_sender_event_data(const OneWayFrameInfo &info, bool linked)
Build the Home Assistant event data map for a decoded 1W sender frame.
constexpr const char * TAG
Shared log tag for hub-level messages.
constexpr size_t NAME_AND_HEX_BUFFER_SIZE
Buffer size for format_name_and_hex(): longest command name plus "(0xXX)" and a margin.
std::string describe_learned_device_type(DeviceType type)
Build the YAML line to add once a device's type is learned at runtime.
constexpr float BINARY_ENTITY_ON_POSITION_THRESHOLD
Shared 0-100 cutoff: values below this mean binary "on".
bool known_device_supports_status_requests(const IoDevice &dev)
Does the device support status requests?
void log_1w_remote_frame(const OneWayFrameInfo &info, const std::vector< std::string > *linked_devices=nullptr)
Log an already-decoded 1W remote frame at DEBUG level.
void record_exchange_timeout(IoDevice &dev, uint8_t tries)
Record that an outbound exchange to this device timed out (no valid response).
void log_component_capture(const RadioDriver *radio, const char *stage, const uint8_t *buf, uint8_t len, const IoFrame *frame=nullptr)
Log a frame at the "io_capture" tag with structured fields.
bool is_exposed_sender(const std::vector< std::string > &exposed_senders, const std::string &sender_id)
Whether a 1W sender is on the exposed_senders allowlist for the sender HA event.
constexpr const char * ONEWAY_SENDER_EVENT
Home Assistant event fired when a decoded 1W frame carries a command intent from an exposed sender (a...
void update_link_health(IoDevice &dev, RadioDriver *radio)
Update per-device link-health stats from the radio's last capture.
uint8_t round_percent(float fraction)
Convert a 0.0-1.0 HA fraction (position, tilt, or brightness) to a 0-100 IO percent.
void normalize_stopped_state(IoDevice &dev)
Normalize stopped state: some devices briefly report stopped before target/current converge.
bool is_binary_entity_position(uint8_t position)
Is the given position value an on/off binary encoding?
constexpr uint32_t ONEWAY_DEDUP_WINDOW_MS
Suppress a repeated 1W log/poll for the same remote and the same intent within this window.
void log_status_update(const std::string &id, const IoDevice &dev, const char *suffix="")
Log a concise status‑update line used by inbound handlers.
constexpr size_t LEARNED_DEVICE_TYPE_HEX_BUFFER_SIZE
Buffer size for describe_learned_device_type()'s hex fallback: "io_device_type: 0xXX" plus margin.
void log_frame_issue(IOHomeControlComponent *component, const char *direction, const char *reason, const IoFrame &frame, uint8_t len)
Log a frame‑level issue (unregistered endpoints, unsupported commands).
void clear_command_result(IoDevice &dev)
Clear a previously recorded CMD_ERROR_RESP result, if any.
void log_command_result(const std::string &id, uint8_t result, uint8_t request_cmd=0, bool include_request_cmd=false)
Log a decoded CMD_ERROR_RESP result with optional request-command context.
void record_command_result(IoDevice &dev, const std::string &id, uint8_t result, uint8_t request_cmd=0, bool include_request_cmd=false)
Store a decoded CMD_ERROR_RESP result on the device and log it.
const char * device_operation_profile_name(DeviceType type)
Human‑readable operation profile name for a device type.
static constexpr float UNKNOWN_POSITION
Sentinel value meaning "position is not known yet".
DeviceType
Device type identifiers reported by IO‑Homecontrol products.
@ UNKNOWN
Unknown/unspecified device.
DeviceCapabilityClass device_capability_class(DeviceType type)
Map a raw IO‑Homecontrol type to the closest ESPHome/Home Assistant entity family.
bool device_supports_position_control(DeviceType type)
Does this device type support precise position control (0–100)?
const char * command_name(uint8_t cmd)
Get a human-readable name for any IO-Homecontrol command ID.
int16_t rssi_scaled_to_dbm(int16_t scaled)
Convert an rssi_ema_scaled fixed-point value to whole dBm (round half away from zero).
const char * device_type_name(DeviceType type)
Convert a DeviceType to a lowercase string identifier.
bool device_supports_binary_control(DeviceType type)
Does this device type support binary on/off control?
std::string format_position(float pos)
Format a position float as a human‑readable string (e.g.
Definition hub_core.h:827
bool device_supports_lock_control(DeviceType type)
Does this device type support binary lock/unlock control via execute commands?
const char * command_result_description(uint8_t result)
Return a human-readable explanation for a CMD_ERROR_RESP result code.
void render_frame_hex_redacted(const uint8_t *data, uint8_t len, char *out, size_t out_size)
Render a frame's bytes as spaced hex text, masking the payload when the command carries key material ...
Definition log_frame.h:71
const char * command_result_name(uint8_t result)
Return a stable symbolic name for a CMD_ERROR_RESP result code.
constexpr size_t FRAME_LOG_HEX_BUFFER_SIZE
Fits a full 32-byte frame rendered as spaced hex text.
Definition log_frame.h:19
static constexpr uint8_t BINARY_ENTITY_ON_POSITION
Position value written for binary ON commands (light on, switch on, lock unlock).
const char * device_capability_class_name(DeviceType type)
Get a human‑readable name for a capability class.
const char * acei_level_name(uint8_t level)
Get a human-readable name for an ACEI priority level (0–7).
std::string node_id_to_string(const uint8_t id[NODE_ID_SIZE])
Format a 3‑byte node ID as a 6‑character uppercase hex string.
static constexpr int16_t RSSI_EMA_SCALE
EMA weight denominator and fixed-point scale for IoDevice::rssi_ema_scaled.
@ BROADCAST_ALL
Broadcast to all devices of a type (address suffix 0x3F).
const char * address_class_name(AddressClass address_class)
Get a human-readable name for an address classification.
bool device_supports_status_requests(DeviceType type)
Does this device type support status request commands (0x03)?
bool has_reached_target_position(float target, float position)
Has the device reached its target within tolerance?
const char * yaml_device_type_name(DeviceType type)
Return the YAML-friendly device-type name for types exposed in the Python schema.
DeviceCapabilityClass
High‑level capability class derived from DeviceType.
static constexpr int16_t RSSI_UNKNOWN_DBM
Sentinel value meaning "no RSSI sample recorded yet" for last_rssi_dbm/rssi_ema_scaled.
bool is_limitation_result(uint8_t result)
Check whether a result code represents an environmental or control limitation.
const char * originator_name(uint8_t originator)
Get a human-readable name for a command originator byte.
static constexpr uint8_t BINARY_ENTITY_OFF_POSITION
Position value written for binary OFF commands (light off, switch off, lock lock).
bool device_supports_tilt(DeviceType type)
Does this device type support tilt (slat angle) control?
Runtime state of a paired IO‑Homecontrol device.
uint32_t last_result_at_ms
millis() timestamp of last_result_code, 0 when none recorded.
float target
Target position the device is moving toward.
uint16_t exchange_attempt_count
Cumulative attempts (ExchangeEngine::DebugInfo::tries, 1-based per exchange) across those timed-out e...
uint8_t last_result_code
Last CMD_ERROR_RESP result byte (0 = none recorded).
int16_t last_rssi_dbm
Most recent raw RSSI sample (dBm), or RSSI_UNKNOWN_DBM.
float position
Current position: 0=open, 100=closed, or UNKNOWN_POSITION.
uint32_t last_seen_ms
millis() of the last frame received from this device (any command), 0 = never.
int16_t rssi_ema_scaled
Smoothed RSSI as fixed point in 1/RSSI_EMA_SCALE dBm — read through device_rssi_ema_dbm(),...
DeviceType type
Device type (shutter, awning, etc.).
uint16_t exchange_timeout_count
Cumulative count of outbound exchanges to this device with no valid response (see detail::record_exch...
bool is_stopped
True if device is not moving.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:71
uint8_t src[NODE_ID_SIZE]
Source node ID (3 bytes).
Definition proto_frame.h:75
uint8_t dst[NODE_ID_SIZE]
Destination node ID (3 bytes).
Definition proto_frame.h:74
uint8_t data_len
Actual length of data.
Definition proto_frame.h:78
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 acei_level
ACEI priority level (0–7).
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).
Diagnostic capture from a radio operation.
uint32_t timestamp_ms
Timestamp of capture (millis).
bool valid
True if capture is valid.
uint32_t freq_hz
RF frequency of capture (Hz).
int16_t rssi_dbm
Received signal strength (dBm).