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///
6/// This header is intentionally private to the component implementation. It keeps
7/// small cross-file helpers in one place while leaving hub_core.h focused on the
8/// public component shape and the member-function declarations.
9
10#include "hub_core.h"
11#include "log_frame.h"
12
13#include "esphome/core/log.h"
14
15#include <cstdio>
16
17namespace esphome {
18namespace home_io_control {
19namespace detail {
20
21// ============================================================================
22// Shared constants
23// ============================================================================
24
25inline constexpr const char *TAG = "home_io_control"; ///< Shared log tag for hub-level messages.
26inline constexpr uint32_t STATUS_RETRY_AFTER_FAIL_MS = 5000; ///< First retry after a silent status-poll failure.
27inline constexpr uint32_t STATUS_RETRY_AFTER_FAIL_STEP2_MS =
28 15000; ///< Second retry after a silent status-poll failure.
29inline constexpr uint32_t STATUS_RETRY_AFTER_FAIL_STEP3_MS =
30 30000; ///< Third retry after a silent status-poll failure.
31inline constexpr uint32_t STATUS_RETRY_AFTER_FAIL_STEP4_MS =
32 60000; ///< Fourth retry after a silent status-poll failure.
33inline constexpr uint32_t STATUS_RETRY_AFTER_FAIL_MAX_MS =
34 300000; ///< Steady-state backoff for repeated silent status-poll failures.
35inline constexpr uint32_t STATUS_AUTH_RETRY_AFTER_FAIL_MS =
36 30000; ///< First retry after a challenge-seen auth-like failure.
37inline constexpr uint32_t STATUS_AUTH_RETRY_AFTER_FAIL_STEP2_MS =
38 120000; ///< Second retry after a challenge-seen auth-like failure.
39inline constexpr uint32_t STATUS_AUTH_RETRY_AFTER_FAIL_MAX_MS =
40 300000; ///< Steady-state backoff after repeated auth-like failures.
41inline constexpr uint32_t INITIAL_STATUS_REQUEST_DELAY_MS =
42 5000; ///< Delay before the first post-boot status request from an entity.
43inline constexpr uint32_t REMOTE_ACTIVITY_STATUS_POLL_DELAY_MS =
44 2000; ///< Delay before polling after overheard remote traffic.
45inline constexpr uint32_t MAX_TRACKED_STATUS_POLL_WINDOW_MS =
46 600000; ///< Hard stop for follow-up polling after a command or remote activity.
47inline constexpr uint32_t PAIRING_DISCOVERY_RESPONSE_TIMEOUT_MS = 2000; ///< Discovery wait window after sending 0x28.
48inline constexpr uint32_t PAIRING_KEY_CHALLENGE_TIMEOUT_MS = 500; ///< Wait window for the device's 0x3C challenge.
50 50.0F; ///< Shared 0-100 cutoff: values below this mean binary "on".
51
52// Binary on/off entities reuse the proven position transport encoding.
53inline constexpr uint8_t BINARY_ENTITY_ON_POSITION = 0;
54inline constexpr uint8_t BINARY_ENTITY_OFF_POSITION = 100;
55
56/// @brief Clear all bounded follow-up polling state for a device.
57/// @param dev Device record to reset.
60 dev.next_update = 0;
61 dev.poll_deadline = 0;
63 dev.auth_poll_failures = 0;
64}
65
66/// @brief Check whether a device remains inside its bounded follow-up polling window.
67/// @param dev Device record.
68/// @param now Current millis() timestamp.
69/// @return true when repeated polling may continue.
70inline bool status_poll_tracking_active(const IoDevice &dev, uint32_t now) {
71 return dev.status_poll_interval_ms != 0 && dev.poll_deadline != 0 && now <= dev.poll_deadline;
72}
73
74// ============================================================================
75// Capability and entity-profile helpers
76// ============================================================================
77
78/// @brief Is the given position value an on/off binary encoding?
79/// @param position Position value to test.
80/// @return true if position equals BINARY_ENTITY_ON_POSITION or BINARY_ENTITY_OFF_POSITION.
81inline bool is_binary_entity_position(uint8_t position) {
82 return position == BINARY_ENTITY_ON_POSITION || position == BINARY_ENTITY_OFF_POSITION;
83}
84
85/// @brief Does the device's type match the expected HA entity class?
86/// UNKNOWN devices always match to keep imported/discovered devices working.
87/// @param dev IoDevice to check.
88/// @param expected Desired capability class (COVER, LIGHT, SWITCH, etc.).
89/// @return true if device type matches or is UNKNOWN.
91 return dev.type == DeviceType::UNKNOWN || device_capability_class(dev.type) == expected;
92}
93
94/// @brief Does the device support status requests?
95/// UNKNOWN devices pass through.
96/// @param dev IoDevice to check.
97/// @return true if device type supports status requests or is UNKNOWN.
101
102/// @brief Can this device accept an execute (position) command?
103/// Checks capability and, for unknown types, allows binary positions for light/switch.
104/// @param dev IoDevice to check.
105/// @param position Position value being sent.
106/// @return true if operation is appropriate for this device type.
107inline bool known_device_accepts_execute_position(const IoDevice &dev, uint8_t position) {
108 if (dev.type == DeviceType::UNKNOWN)
109 return true;
111 return true;
113}
114
115/// @brief Can this device accept a tilt command?
116/// @param dev IoDevice to check.
117/// @return true only if device type is known to support tilt.
120}
121
122/// @brief Compute the next background status-poll retry delay after a failed exchange.
123///
124/// Plain silence is treated as a soft reachability problem and ramps up gradually so sleeping
125/// or temporarily busy devices are retried soon. Exchanges that reached the 0x3C challenge but
126/// never completed are much more likely to represent an invalid system key or pairing mismatch,
127/// so they back off more aggressively to avoid repeated 0x3D HMAC traffic.
128///
129/// @param consecutive_failures 1-based count of consecutive failures in the current failure class.
130/// @param auth_like_failure True when the failed exchange saw a 0x3C challenge.
131/// @return Delay in milliseconds before the next automatic status poll.
132inline uint32_t status_poll_retry_delay_ms(uint8_t consecutive_failures, bool auth_like_failure) {
133 if (auth_like_failure) {
134 if (consecutive_failures <= 1)
136 if (consecutive_failures == 2)
139 }
140
141 if (consecutive_failures <= 1)
143 if (consecutive_failures == 2)
145 if (consecutive_failures == 3)
147 if (consecutive_failures == 4)
150}
151
152// ============================================================================
153// Logging helpers
154// ============================================================================
155
156/// @brief Log a rejected operation with capability mismatch details.
157/// @param device_id Device ID string.
158/// @param dev IoDevice that rejected the command.
159/// @param operation Human‑readable operation name (e.g., "set position").
160/// @param expected Expected capability class or profile name.
161inline void log_rejected_operation(const std::string &device_id, const IoDevice &dev, const char *operation,
162 const char *expected) {
163 ESP_LOGW(TAG, "Rejecting %s for device %s: type=%s (%u) class=%s profile=%s expected=%s", operation,
164 device_id.c_str(), device_type_name(dev.type), static_cast<uint8_t>(dev.type),
166}
167
168/// @brief Log a frame at the "io_capture" tag with structured fields.
169/// Used for protocol‑level debugging (phases: component, tx, rx, parse_ok/parse_fail).
170/// @param radio Radio driver instance (provides chip name and capture).
171/// @param stage String label for the current phase.
172/// @param buf Raw bytes being logged.
173/// @param len Length of buf.
174/// @param frame Optional parsed IoFrame for decoded fields (cmd, src, dst).
175inline void log_component_capture(const RadioDriver *radio, const char *stage, const uint8_t *buf, uint8_t len,
176 const IoFrame *frame = nullptr) {
177 const RadioCaptureInfo &capture = radio->get_last_capture();
178 char payload_hex[FRAME_LOG_HEX_BUFFER_SIZE];
179 bytes_to_hex(buf, len, payload_hex, sizeof(payload_hex));
180 if (frame != nullptr) {
181 ESP_LOGD(
182 "io_capture",
183 "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",
184 radio->chip_name(), stage, capture.freq_hz, capture.timestamp_ms, len, frame->cmd, frame->src[0], frame->src[1],
185 frame->src[2], frame->dst[0], frame->dst[1], frame->dst[2], payload_hex);
186 return;
187 }
188 ESP_LOGD("io_capture", "chip=%s phase=component stage=%s freq=%u ts=%u len=%u payload=%s", radio->chip_name(), stage,
189 capture.freq_hz, capture.timestamp_ms, len, payload_hex);
190}
191
192/// @brief Log a frame‑level issue (unregistered endpoints, unsupported commands).
193/// @param component Pointer to the component (for device lookup).
194/// @param direction "tx" or "rx".
195/// @param reason Short issue label (e.g., "unregistered_device").
196/// @param frame Parsed frame.
197/// @param len Serialized length.
198inline void log_frame_issue(IOHomeControlComponent *component, const char *direction, const char *reason,
199 const IoFrame &frame, uint8_t len) {
200 const std::string src_id = node_id_to_string(frame.src);
201 const std::string dst_id = node_id_to_string(frame.dst);
202 const bool src_registered = component->get_device(src_id) != nullptr;
203 const bool dst_registered = component->get_device(dst_id) != nullptr;
204
205 if (src_registered || dst_registered) {
206 ESP_LOGW(TAG, "%s issue=%s cmd=0x%02X src=%s%s dst=%s%s len=%u data_len=%u", direction, reason, frame.cmd,
207 src_id.c_str(), src_registered ? " (registered)" : "", dst_id.c_str(),
208 dst_registered ? " (registered)" : "", len, frame.data_len);
209 return;
210 }
211
212 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(),
213 dst_id.c_str(), len, frame.data_len);
214}
215
216// ============================================================================
217// Status normalization helpers
218// ============================================================================
219
220/// @brief Normalize stopped state: some devices briefly report stopped before target/current converge.
221/// @param dev Device record to update (may clear is_stopped if positions differ).
223 // Some devices briefly report STATUS_STOPPED before current and target have numerically
224 // converged. Keep the device in the moving state until the decoded values are effectively equal.
225 if (dev.is_stopped && dev.target != UNKNOWN_POSITION && dev.position != UNKNOWN_POSITION &&
227 dev.is_stopped = false;
228 }
229}
230
231/// @brief Log a concise status‑update line used by inbound handlers.
232/// @param id Device ID.
233/// @param dev Current device state.
234/// @param suffix Optional suffix added after the state string (e.g., " (status update)").
235inline void log_status_update(const std::string &id, const IoDevice &dev, const char *suffix = "") {
236 ESP_LOGI(TAG, "Device %s: position=%s target=%s %s%s", id.c_str(), format_position(dev.position).c_str(),
237 format_position(dev.target).c_str(), dev.is_stopped ? "stopped" : "moving", suffix);
238}
239
240} // namespace detail
241} // namespace home_io_control
242} // namespace esphome
The main IO-Homecontrol component.
Definition hub_core.h:60
virtual IoDevice * get_device(const std::string &device_id)
Retrieve a device by ID; returns nullptr if not found.
Definition hub_core.cpp:256
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).
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:16
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:427
constexpr size_t FRAME_LOG_HEX_BUFFER_SIZE
Fits a full 32-byte frame rendered as spaced hex text.
Definition log_frame.h:14
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 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).