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 <cstdio>
17
18namespace esphome {
19namespace home_io_control {
20namespace detail {
21
22// ============================================================================
23// Shared constants
24// ============================================================================
25
26inline constexpr const char *TAG = "home_io_control"; ///< Shared log tag for hub-level messages.
27inline constexpr uint32_t STATUS_RETRY_AFTER_FAIL_MS = 5000; ///< First retry after a silent status-poll failure.
28inline constexpr uint32_t STATUS_RETRY_AFTER_FAIL_STEP2_MS =
29 15000; ///< Second retry after a silent status-poll failure.
30inline constexpr uint32_t STATUS_RETRY_AFTER_FAIL_STEP3_MS =
31 30000; ///< Third retry after a silent status-poll failure.
32inline constexpr uint32_t STATUS_RETRY_AFTER_FAIL_STEP4_MS =
33 60000; ///< Fourth retry after a silent status-poll failure.
34inline constexpr uint32_t STATUS_RETRY_AFTER_FAIL_MAX_MS =
35 300000; ///< Steady-state backoff for repeated silent status-poll failures.
36inline constexpr uint32_t STATUS_AUTH_RETRY_AFTER_FAIL_MS =
37 30000; ///< First retry after a challenge-seen auth-like failure.
38inline constexpr uint32_t STATUS_AUTH_RETRY_AFTER_FAIL_STEP2_MS =
39 120000; ///< Second retry after a challenge-seen auth-like failure.
40inline constexpr uint32_t STATUS_AUTH_RETRY_AFTER_FAIL_MAX_MS =
41 300000; ///< Steady-state backoff after repeated auth-like failures.
42inline constexpr uint32_t INITIAL_STATUS_REQUEST_DELAY_MS =
43 5000; ///< Delay before the first post-boot status request from an entity.
44inline constexpr uint32_t REMOTE_ACTIVITY_STATUS_POLL_DELAY_MS =
45 2000; ///< Delay before polling after overheard remote traffic.
46inline constexpr uint32_t MAX_TRACKED_STATUS_POLL_WINDOW_MS =
47 600000; ///< Hard stop for follow-up polling after a command or remote activity.
48inline constexpr uint32_t PAIRING_DISCOVERY_RESPONSE_TIMEOUT_MS = 2000; ///< Discovery wait window after sending 0x28.
49inline constexpr uint32_t PAIRING_KEY_CHALLENGE_TIMEOUT_MS = 500; ///< Wait window for the device's 0x3C challenge.
51 50.0F; ///< Shared 0-100 cutoff: values below this mean binary "on".
52
53// Binary on/off entities reuse the proven position transport encoding.
54inline constexpr uint8_t BINARY_ENTITY_ON_POSITION = 0;
55inline constexpr uint8_t BINARY_ENTITY_OFF_POSITION = 100;
56
57/// @brief Clear all bounded follow-up polling state for a device.
58/// @param dev Device record to reset.
61 dev.next_update = 0;
62 dev.poll_deadline = 0;
64 dev.auth_poll_failures = 0;
65}
66
67/// @brief Check whether a device remains inside its bounded follow-up polling window.
68/// @param dev Device record.
69/// @param now Current millis() timestamp.
70/// @return true when repeated polling may continue.
71inline bool status_poll_tracking_active(const IoDevice &dev, uint32_t now) {
72 return dev.status_poll_interval_ms != 0 && dev.poll_deadline != 0 && now <= dev.poll_deadline;
73}
74
75// ============================================================================
76// Capability and entity-profile helpers
77// ============================================================================
78
79/// @brief Is the given position value an on/off binary encoding?
80/// @param position Position value to test.
81/// @return true if position equals BINARY_ENTITY_ON_POSITION or BINARY_ENTITY_OFF_POSITION.
82inline bool is_binary_entity_position(uint8_t position) {
83 return position == BINARY_ENTITY_ON_POSITION || position == BINARY_ENTITY_OFF_POSITION;
84}
85
86/// @brief Does the device's type match the expected HA entity class?
87/// UNKNOWN devices always match to keep imported/discovered devices working.
88/// @param dev IoDevice to check.
89/// @param expected Desired capability class (COVER, LIGHT, SWITCH, etc.).
90/// @return true if device type matches or is UNKNOWN.
92 return dev.type == DeviceType::UNKNOWN || device_capability_class(dev.type) == expected;
93}
94
95/// @brief Does the device support status requests?
96/// UNKNOWN devices pass through.
97/// @param dev IoDevice to check.
98/// @return true if device type supports status requests or is UNKNOWN.
102
103/// @brief Can this device accept an execute (position) command?
104/// Checks capability and, for unknown types, allows binary positions for light/switch.
105/// @param dev IoDevice to check.
106/// @param position Position value being sent.
107/// @return true if operation is appropriate for this device type.
108inline bool known_device_accepts_execute_position(const IoDevice &dev, uint8_t position) {
109 if (dev.type == DeviceType::UNKNOWN)
110 return true;
112 return true;
113 return is_binary_entity_position(position) &&
115}
116
117/// @brief Can this device accept a tilt command?
118/// @param dev IoDevice to check.
119/// @return true only if device type is known to support tilt.
122}
123
124/// @brief Compute the next background status-poll retry delay after a failed exchange.
125///
126/// Plain silence is treated as a soft reachability problem and ramps up gradually so sleeping
127/// or temporarily busy devices are retried soon. Exchanges that reached the 0x3C challenge but
128/// never completed are much more likely to represent an invalid system key or pairing mismatch,
129/// so they back off more aggressively to avoid repeated 0x3D HMAC traffic.
130///
131/// @param consecutive_failures 1-based count of consecutive failures in the current failure class.
132/// @param auth_like_failure True when the failed exchange saw a 0x3C challenge.
133/// @return Delay in milliseconds before the next automatic status poll.
134inline uint32_t status_poll_retry_delay_ms(uint8_t consecutive_failures, bool auth_like_failure) {
135 if (auth_like_failure) {
136 if (consecutive_failures <= 1)
138 if (consecutive_failures == 2)
141 }
142
143 if (consecutive_failures <= 1)
145 if (consecutive_failures == 2)
147 if (consecutive_failures == 3)
149 if (consecutive_failures == 4)
152}
153
154// ============================================================================
155// Logging helpers
156// ============================================================================
157
158/// @brief Log a rejected operation with capability mismatch details.
159/// @param device_id Device ID string.
160/// @param dev IoDevice that rejected the command.
161/// @param operation Human‑readable operation name (e.g., "set position").
162/// @param expected Expected capability class or profile name.
163inline void log_rejected_operation(const std::string &device_id, const IoDevice &dev, const char *operation,
164 const char *expected) {
165 ESP_LOGW(TAG, "Rejecting %s for device %s: type=%s (%u) class=%s profile=%s expected=%s", operation,
166 device_id.c_str(), device_type_name(dev.type), static_cast<uint8_t>(dev.type),
168}
169
170/// @brief Log a frame at the "io_capture" tag with structured fields.
171/// Used for protocol‑level debugging (phases: component, tx, rx, parse_ok/parse_fail).
172/// @param radio Radio driver instance (provides chip name and capture).
173/// @param stage String label for the current phase.
174/// @param buf Raw bytes being logged.
175/// @param len Length of buf.
176/// @param frame Optional parsed IoFrame for decoded fields (cmd, src, dst).
177inline void log_component_capture(const RadioDriver *radio, const char *stage, const uint8_t *buf, uint8_t len,
178 const IoFrame *frame = nullptr) {
179 const RadioCaptureInfo &capture = radio->get_last_capture();
180 char payload_hex[FRAME_LOG_HEX_BUFFER_SIZE];
181 bytes_to_hex(buf, len, payload_hex, sizeof(payload_hex));
182 if (frame != nullptr) {
183 ESP_LOGD(
184 "io_capture",
185 "chip=%s phase=component stage=%s freq=%u ts=%u len=%u cmd=0x%02X src=%02X%02X%02X dst=%02X%02X%02X payload=%s",
186 radio->chip_name(), stage, capture.freq_hz, capture.timestamp_ms, len, frame->cmd, frame->src[0], frame->src[1],
187 frame->src[2], frame->dst[0], frame->dst[1], frame->dst[2], payload_hex);
188 return;
189 }
190 ESP_LOGD("io_capture", "chip=%s phase=component stage=%s freq=%u ts=%u len=%u payload=%s", radio->chip_name(), stage,
191 capture.freq_hz, capture.timestamp_ms, len, payload_hex);
192}
193
194/// @brief Log a frame‑level issue (unregistered endpoints, unsupported commands).
195/// @param component Pointer to the component (for device lookup).
196/// @param direction "tx" or "rx".
197/// @param reason Short issue label (e.g., "unregistered_device").
198/// @param frame Parsed frame.
199/// @param len Serialized length.
200inline void log_frame_issue(IOHomeControlComponent *component, const char *direction, const char *reason,
201 const IoFrame &frame, uint8_t len) {
202 const std::string src_id = node_id_to_string(frame.src);
203 const std::string dst_id = node_id_to_string(frame.dst);
204 const bool src_registered = component->get_device(src_id) != nullptr;
205 const bool dst_registered = component->get_device(dst_id) != nullptr;
206
207 if (src_registered || dst_registered) {
208 ESP_LOGW(TAG, "%s issue=%s cmd=0x%02X src=%s%s dst=%s%s len=%u data_len=%u", direction, reason, frame.cmd,
209 src_id.c_str(), src_registered ? " (registered)" : "", dst_id.c_str(),
210 dst_registered ? " (registered)" : "", len, frame.data_len);
211 return;
212 }
213
214 ESP_LOGD(TAG, "%s issue=%s cmd=0x%02X src=%s dst=%s len=%u data_len=%u", direction, reason, frame.cmd, src_id.c_str(),
215 dst_id.c_str(), len, frame.data_len);
216}
217
218// ============================================================================
219// Status normalization helpers
220// ============================================================================
221
222/// @brief Normalize stopped state: some devices briefly report stopped before target/current converge.
223/// @param dev Device record to update (may clear is_stopped if positions differ).
225 // Some devices briefly report STATUS_STOPPED before current and target have numerically
226 // converged. Keep the device in the moving state until the decoded values are effectively equal.
227 if (dev.is_stopped && dev.target != UNKNOWN_POSITION && dev.position != UNKNOWN_POSITION &&
229 dev.is_stopped = false;
230 }
231}
232
233/// @brief Log a concise status‑update line used by inbound handlers.
234/// @param id Device ID.
235/// @param dev Current device state.
236/// @param suffix Optional suffix added after the state string (e.g., " (status update)").
237inline void log_status_update(const std::string &id, const IoDevice &dev, const char *suffix = "") {
238 ESP_LOGI(TAG, "Device %s: position=%s target=%s %s%s", id.c_str(), format_position(dev.position).c_str(),
239 format_position(dev.target).c_str(), dev.is_stopped ? "stopped" : "moving", suffix);
240}
241
242/// @brief Log a decoded CMD_ERROR_RESP result with optional request-command context.
243/// @param id Device ID.
244/// @param result Result byte from CMD_ERROR_RESP data[0].
245/// @param request_cmd Original outbound request command when known.
246/// @param include_request_cmd True to include request_cmd in the log line.
247inline void log_command_result(const std::string &id, uint8_t result, uint8_t request_cmd = 0,
248 bool include_request_cmd = false) {
249 const char *kind = is_limitation_result(result) ? "limitation" : "error";
250 if (include_request_cmd) {
251 ESP_LOGW(TAG, "Device %s: command 0x%02X returned %s result=0x%02X %s (%s)", id.c_str(), request_cmd, kind, result,
253 return;
254 }
255
256 ESP_LOGW(TAG, "Device %s: explicit %s result=0x%02X %s (%s)", id.c_str(), kind, result, command_result_name(result),
258}
259
260} // namespace detail
261} // namespace home_io_control
262} // namespace esphome
The main IO-Homecontrol component.
Definition hub_core.h:68
virtual IoDevice * get_device(const std::string &device_id)
Retrieve a device by ID; returns nullptr if not found.
Definition hub_core.cpp:258
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?
constexpr uint32_t STATUS_AUTH_RETRY_AFTER_FAIL_STEP2_MS
Second retry after a challenge-seen auth-like failure.
bool known_device_matches_entity_class(const IoDevice &dev, DeviceCapabilityClass expected)
Does the device's type match the expected HA entity class?
constexpr uint32_t STATUS_RETRY_AFTER_FAIL_STEP4_MS
Fourth retry after a silent status-poll failure.
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?
constexpr uint32_t STATUS_RETRY_AFTER_FAIL_MS
First retry after a silent status-poll failure.
constexpr uint32_t STATUS_AUTH_RETRY_AFTER_FAIL_MAX_MS
Steady-state backoff after repeated auth-like failures.
constexpr const char * TAG
Shared log tag for hub-level messages.
constexpr uint32_t INITIAL_STATUS_REQUEST_DELAY_MS
Delay before the first post-boot status request from an entity.
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_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.
constexpr uint32_t PAIRING_DISCOVERY_RESPONSE_TIMEOUT_MS
Discovery wait window after sending 0x28.
constexpr uint32_t STATUS_RETRY_AFTER_FAIL_STEP2_MS
Second retry after a silent status-poll failure.
constexpr uint32_t PAIRING_KEY_CHALLENGE_TIMEOUT_MS
Wait window for the device's 0x3C challenge.
constexpr uint8_t BINARY_ENTITY_ON_POSITION
constexpr uint32_t STATUS_RETRY_AFTER_FAIL_MAX_MS
Steady-state backoff for repeated silent status-poll failures.
constexpr uint8_t BINARY_ENTITY_OFF_POSITION
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?
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 uint32_t REMOTE_ACTIVITY_STATUS_POLL_DELAY_MS
Delay before polling after overheard remote traffic.
constexpr uint32_t STATUS_RETRY_AFTER_FAIL_STEP3_MS
Third retry after a silent status-poll failure.
constexpr uint32_t MAX_TRACKED_STATUS_POLL_WINDOW_MS
Hard stop for follow-up polling after a command or remote activity.
uint32_t status_poll_retry_delay_ms(uint8_t consecutive_failures, bool auth_like_failure)
Compute the next background status-poll retry delay after a failed exchange.
void clear_status_poll_tracking(IoDevice &dev)
Clear all bounded follow-up polling state for a device.
bool status_poll_tracking_active(const IoDevice &dev, uint32_t now)
Check whether a device remains inside its bounded follow-up polling window.
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 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.
constexpr uint32_t STATUS_AUTH_RETRY_AFTER_FAIL_MS
First retry after a challenge-seen auth-like failure.
const char * device_operation_profile_name(DeviceType type)
Human‑readable operation profile name for a device type.
void bytes_to_hex(const uint8_t *data, uint8_t len, char *out, size_t out_size)
Definition log_frame.h:17
static constexpr float UNKNOWN_POSITION
Sentinel value meaning "position is not known yet".
@ 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 * 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:486
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.
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:15
const char * device_capability_class_name(DeviceType type)
Get a human‑readable name for a capability class.
std::string node_id_to_string(const uint8_t id[NODE_ID_SIZE])
Format a 3‑byte node ID as a 6‑character uppercase hex string.
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?
DeviceCapabilityClass
High‑level capability class derived from DeviceType.
bool is_limitation_result(uint8_t result)
Check whether a result code represents an environmental or control limitation.
bool device_supports_tilt(DeviceType type)
Does this device type support tilt (slat angle) control?
Runtime state of a paired IO‑Homecontrol device.
float target
Target position the device is moving toward.
uint32_t status_poll_interval_ms
Configured follow-up poll interval while a state change is expected.
uint32_t next_update
millis() timestamp when we should poll for status next.
uint8_t status_poll_failures
Consecutive background status-poll failures without a valid reply.
uint8_t auth_poll_failures
Consecutive background poll failures that reached 0x3C auth challenge.
float position
Current position: 0=open, 100=closed, or UNKNOWN_POSITION.
bool single_follow_up_poll_pending
True when one legacy settle poll should still be scheduled.
DeviceType type
Device type (shutter, awning, etc.).
bool is_stopped
True if device is not moving.
uint32_t poll_deadline
Hard stop for bounded follow-up polling after a command or remote activity.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
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.
Diagnostic capture from a radio operation.
uint32_t timestamp_ms
Timestamp of capture (millis).
uint32_t freq_hz
RF frequency of capture (Hz).