23constexpr uint8_t PRIVATE_RESPONSE_MIN_DATA_LEN = 8;
24constexpr uint8_t STATUS_UPDATE_MIN_DATA_LEN = 11;
25constexpr uint8_t GET_INFO2_RESPONSE_MIN_DATA_LEN = 12;
26constexpr uint8_t EXTENDED_TILT_RESPONSE_MIN_DATA_LEN =
28constexpr uint8_t STATUS_STOPPED_FLAGS_OFFSET = 0;
29constexpr uint8_t PRIVATE_RESPONSE_DELAY_HINT_OFFSET = 7;
30constexpr uint8_t PRIVATE_RESPONSE_TARGET_OFFSET = 2;
31constexpr uint8_t PRIVATE_RESPONSE_CURRENT_OFFSET = 4;
32constexpr uint8_t STATUS_UPDATE_TARGET_OFFSET = 5;
33constexpr uint8_t STATUS_UPDATE_CURRENT_OFFSET = 7;
34constexpr uint8_t GET_INFO2_TYPE_OFFSET = 10;
35constexpr uint8_t GET_INFO2_TYPE_SUBTYPE_OFFSET = 11;
36constexpr uint8_t EXTENDED_TILT_SELECTOR_OFFSET = 12;
37constexpr uint8_t EXTENDED_TILT_MSB_OFFSET = 13;
38constexpr uint8_t EXTENDED_TILT_LSB_OFFSET = 14;
39constexpr uint8_t PRIVATE_RESPONSE_HINT_UNUSED = 0xFF;
40constexpr uint8_t PRIVATE_RESPONSE_HINT_ZERO =
42constexpr uint32_t PRIVATE_RESPONSE_HINT_SCALE_MS = 1000;
43constexpr uint32_t PRIVATE_RESPONSE_HINT_BIAS_MS =
45constexpr uint32_t DEFAULT_SINGLE_FOLLOW_UP_POLL_DELAY_MS =
55void decode_status_fields(
IoDevice &dev,
const IoFrame &frame, uint8_t target_offset, uint8_t current_offset,
56 bool allow_tilt_from_extended_response) {
57 uint16_t
const tgt = (frame.data[target_offset] << 8) | frame.data[target_offset + 1];
58 uint16_t
const cur = (frame.data[current_offset] << 8) | frame.data[current_offset + 1];
63 frame.data_len >= EXTENDED_TILT_RESPONSE_MIN_DATA_LEN &&
65 uint16_t
const tilt_raw = (frame.data[EXTENDED_TILT_MSB_OFFSET] << 8) | frame.data[EXTENDED_TILT_LSB_OFFSET];
74uint32_t compute_private_response_delay_ms(
const IoDevice &dev,
const IoFrame &frame) {
82 if (frame.data[PRIVATE_RESPONSE_DELAY_HINT_OFFSET] != PRIVATE_RESPONSE_HINT_UNUSED &&
83 frame.data[PRIVATE_RESPONSE_DELAY_HINT_OFFSET] != PRIVATE_RESPONSE_HINT_ZERO) {
84 uint32_t
const hinted_delay_ms =
85 frame.data[PRIVATE_RESPONSE_DELAY_HINT_OFFSET] * PRIVATE_RESPONSE_HINT_SCALE_MS + PRIVATE_RESPONSE_HINT_BIAS_MS;
86 if (dev.status_poll_interval_ms == 0)
87 return hinted_delay_ms;
88 return hinted_delay_ms < dev.status_poll_interval_ms ? hinted_delay_ms : dev.status_poll_interval_ms;
90 return dev.status_poll_interval_ms != 0 ? dev.status_poll_interval_ms : DEFAULT_SINGLE_FOLLOW_UP_POLL_DELAY_MS;
96uint32_t compute_status_update_delay_ms(
const IoDevice &dev) {
97 return dev.is_stopped ? 0 : dev.status_poll_interval_ms;
103void apply_private_response_status(
IoDevice &dev,
const IoFrame &frame) {
104 dev.is_stopped = (frame.data[STATUS_STOPPED_FLAGS_OFFSET] &
STATUS_STOPPED) != 0;
105 dev.last_status = millis();
106 decode_status_fields(dev, frame, PRIVATE_RESPONSE_TARGET_OFFSET, PRIVATE_RESPONSE_CURRENT_OFFSET,
true);
109 if (dev.is_stopped || !tracked_polling_active) {
110 if (!dev.is_stopped && dev.single_follow_up_poll_pending) {
111 dev.next_update = dev.last_status + compute_private_response_delay_ms(dev, frame);
112 dev.single_follow_up_poll_pending =
false;
119 dev.next_update = dev.last_status + compute_private_response_delay_ms(dev, frame);
125void apply_unsolicited_status_update(
IoDevice &dev,
const IoFrame &frame) {
126 dev.is_stopped = (frame.data[STATUS_STOPPED_FLAGS_OFFSET] &
STATUS_STOPPED) != 0;
127 dev.last_status = millis();
128 decode_status_fields(dev, frame, STATUS_UPDATE_TARGET_OFFSET, STATUS_UPDATE_CURRENT_OFFSET,
false);
135 dev.next_update = dev.last_status + compute_status_update_delay_ms(dev);
155 if (dev ==
nullptr || dev->status_poll_interval_ms == 0)
158 uint32_t
const now = millis();
160 dev->next_update = initial_delay_ms == 0 ? 0 : now + initial_delay_ms;
161 dev->status_poll_failures = 0;
162 dev->auth_poll_failures = 0;
168 const std::string timeout_name =
"remote_poll_" + device_id;
169 this->set_timeout(timeout_name.c_str(), delay_ms,
170 [
this, device_id]() { this->queue_request_device_status(device_id); });
183 if (frame.
data_len < PRIVATE_RESPONSE_MIN_DATA_LEN) {
191 apply_private_response_status(dev, frame);
198 if (frame.
data_len < STATUS_UPDATE_MIN_DATA_LEN) {
205 apply_unsolicited_status_update(dev, frame);
212 if (frame.
data_len < GET_INFO2_RESPONSE_MIN_DATA_LEN) {
219 apply_info2_response(dev, frame);
220 ESP_LOGI(
detail::TAG,
"Device %s: type=%s (%u) class=%s profile=%s subtype=%u",
id.c_str(),
281 if (
auto *dev = this->
get_device(dst_id); dev !=
nullptr)
282 dev->single_follow_up_poll_pending = dev->status_poll_interval_ms == 0;
283 ESP_LOGD(
detail::TAG,
"rx remote_activity src=%s dst=%s cmd=0x%02X, scheduling status poll",
296 for (
const auto &device_id : remote_it->second) {
297 if (
auto *dev = this->
get_device(device_id); dev !=
nullptr)
298 dev->single_follow_up_poll_pending = dev->status_poll_interval_ms == 0;
299 ESP_LOGD(
detail::TAG,
"rx remote_activity (linked) remote=%s device=%s cmd=0x%02X, scheduling status poll",
300 src_id.c_str(), device_id.c_str(), frame.
cmd);
std::map< std::string, IoDevice > devices_
void begin_status_poll_tracking_(const std::string &device_id, uint32_t initial_delay_ms)
Begin bounded follow-up polling for a device after a command or overheard remote activity.
virtual IoDevice * get_device(const std::string &device_id)
Retrieve a device by ID; returns nullptr if not found.
void schedule_status_poll_(const std::string &device_id, uint32_t delay_ms)
Schedule a delayed status poll for a registered device using the Component timeout API.
bool transmit_frame_(const IoFrame &frame, uint32_t freq, uint16_t preamble)
Transmit a raw IoFrame on the current frequency with given preamble length.
void update_device_status_(const IoFrame &frame)
Extract position/status info from a status or status-update frame and merge into device record.
void notify_device_update_(const std::string &id)
Fire all registered device update callbacks for the given device ID.
std::map< std::string, std::vector< std::string > > linked_remotes_
Maps remote node IDs to lists of device IDs they control.
uint8_t node_id_[NODE_ID_SIZE]
void process_received_packet_(const RadioRxPacket &packet)
Parse received packet, update device state if it's a status frame, and notify covers.
bool authenticate_request_(const IoFrame &request, uint32_t freq)
Handle an inbound authenticated command from a device (status updates, etc.).
Pure transition helpers for hub-owned exchange and pairing frame decisions.
Internal helpers shared by the hub implementation .cpp files.
bool is_exchange_internal_command(uint8_t cmd)
Returns true for commands that are internal to an exchange handshake and carry no useful information ...
constexpr const char * TAG
Shared log tag for hub-level messages.
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.
void normalize_stopped_state(IoDevice &dev)
Normalize stopped state: some devices briefly report stopped before target/current converge.
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 MAX_TRACKED_STATUS_POLL_WINDOW_MS
Hard stop for follow-up polling after a command or remote activity.
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).
const char * device_operation_profile_name(DeviceType type)
Human‑readable operation profile name for a device type.
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
@ UNKNOWN
Unknown/unspecified device.
static constexpr uint8_t CMD_STATUS_UPDATE
Device-initiated status update (needs auth).
bool default_inverted_for_type(DeviceType type)
Determine whether a device type has inverted position mapping by default.
static constexpr uint32_t FREQ_CH1
The protocol uses 3 frequency channels in the 868 MHz ISM band.
static constexpr uint32_t FREQ_CH3
Channel 3: 869.85 MHz (2W only).
const char * device_type_name(DeviceType type)
Convert a DeviceType to a lowercase string identifier.
float decode_tilt_report(uint16_t tilt_raw)
Decode tilt angle from raw 16‑bit value.
uint8_t frame_length(const IoFrame &f)
Get total frame length from ctrl0.
DeviceType decode_packed_device_type(uint8_t type_msb, uint8_t type_subtype)
Decode a protocol-packed device type from two metadata bytes.
bool parse(const uint8_t *buf, uint8_t buf_len, IoFrame &f)
Parse a wire buffer into a parsed IoFrame (validates length and CTRL0).
static constexpr uint8_t CMD_PRIVATE_RESP
Response to 0x00 and 0x03 (contains position data).
const char * device_capability_class_name(DeviceType type)
Get a human‑readable name for a capability class.
static constexpr uint32_t FREQ_CH2
Channel 2: 868.95 MHz (1W and 2W, TX channel).
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.
uint8_t decode_packed_device_subtype(uint8_t type_subtype)
Decode a protocol-packed device subtype from the second metadata byte.
static constexpr uint8_t STATUS_TILT_SELECTOR
Extended status payload marker for tilt-capable devices.
static constexpr uint16_t SHORT_PREAMBLE
8 bytes for response/continuation frames
static constexpr uint8_t CMD_GET_INFO2_RESP
Device type/model response.
void decode_position_report(uint16_t target_raw, uint16_t current_raw, bool is_stopped, float &target, float &position)
Decode target/current position values from a status frame.
bool device_supports_tilt(DeviceType type)
Does this device type support tilt (slat angle) control?
bool create_status_update_resp(IoFrame &f, const uint8_t *own, const uint8_t *dst)
Build a status-update acknowledgment (0x72).
static constexpr uint8_t STATUS_STOPPED
Status byte flags in CMD_PRIVATE_RESP and CMD_STATUS_UPDATE.
Command builders for the IO‑Homecontrol protocol.
Runtime state of a paired IO‑Homecontrol device.
uint8_t subtype
Device subtype (manufacturer‑specific).
DeviceType type
Device type (shutter, awning, etc.).
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.
Raw packet received from the radio.
uint8_t len
Length of packet in bytes.
uint32_t freq_hz
Frequency the packet was received on (Hz).
uint8_t data[RADIO_PACKET_BUFFER_SIZE]
Raw packet data buffer.