36constexpr uint8_t PRIVATE_RESPONSE_MIN_DATA_LEN = 6;
37constexpr uint8_t STATUS_UPDATE_MIN_DATA_LEN = 11;
38constexpr uint8_t GET_NAME_RESPONSE_MIN_DATA_LEN = 1;
39constexpr uint8_t GET_INFO2_RESPONSE_MIN_DATA_LEN = 12;
40constexpr uint8_t ERROR_RESPONSE_MIN_DATA_LEN = 1;
41constexpr uint8_t EXTENDED_TILT_RESPONSE_MIN_DATA_LEN =
43constexpr uint8_t STATUS_STOPPED_FLAGS_OFFSET = 0;
44constexpr uint8_t STATUS_BATTERY_OFFSET = 1;
45constexpr uint8_t PRIVATE_RESPONSE_REMAINING_TIME_OFFSET =
57constexpr uint16_t MAIN_PARAMETER_LOW_BYTE_MASK = 0xFF;
58constexpr uint8_t PRIVATE_RESPONSE_TARGET_OFFSET = 2;
59constexpr uint8_t PRIVATE_RESPONSE_CURRENT_OFFSET = 4;
60constexpr uint8_t STATUS_UPDATE_TARGET_OFFSET = 5;
61constexpr uint8_t STATUS_UPDATE_CURRENT_OFFSET = 7;
64 "the target position is the first byte of the status-change event data");
66 "the last-command record is part of the status-change event data");
67constexpr uint8_t GET_INFO2_TYPE_OFFSET = 10;
68constexpr uint8_t GET_INFO2_TYPE_SUBTYPE_OFFSET = 11;
69constexpr uint8_t EXTENDED_TILT_SELECTOR_OFFSET = 12;
70constexpr uint8_t EXTENDED_TILT_MSB_OFFSET = 13;
71constexpr uint8_t EXTENDED_TILT_LSB_OFFSET = 14;
72constexpr uint8_t PRIVATE_RESPONSE_REMAINING_TIME_MIN_DATA_LEN =
74constexpr uint16_t PRIVATE_RESPONSE_REMAINING_TIME_NONE = 0xFFFF;
75constexpr uint32_t PRIVATE_RESPONSE_REMAINING_TIME_SCALE_MS = 1000;
76constexpr uint32_t PRIVATE_RESPONSE_REMAINING_TIME_BIAS_MS =
92 in.target = dev.target;
93 in.position = dev.position;
94 in.candidate = dev.stop_candidate;
95 in.follow_up_possible = follow_up_possible;
99 ESP_LOGD(
detail::TAG,
"Device %s: reports stopped at %.2f%%, target %.2f%%: confirming with one more poll",
100 id.c_str(),
static_cast<double>(dev.position),
static_cast<double>(dev.target));
104 ESP_LOGD(
detail::TAG,
"Device %s: stopped at %.2f%% again, target %.2f%%: at rest off target",
id.c_str(),
105 static_cast<double>(dev.position),
static_cast<double>(dev.target));
108 "Device %s: reports stopped at %.2f%%, target %.2f%%, no poll to confirm by: taken as at rest",
id.c_str(),
109 static_cast<double>(dev.position),
static_cast<double>(dev.target));
112 dev.stop_candidate.target = dev.target;
113 dev.stop_candidate.position = dev.position;
115 dev.stop_candidate.clear();
131void decode_status_fields(
IoDevice &dev,
const IoFrame &frame, uint8_t target_offset, uint8_t current_offset,
132 bool allow_tilt_from_extended_response,
const std::string &
id,
StatusPollPolicy &policy) {
133 uint16_t
const tgt = (frame.data[target_offset] << 8) | frame.data[target_offset + 1];
134 uint16_t
const cur = (frame.data[current_offset] << 8) | frame.data[current_offset + 1];
139 policy.set_confirm_stopped(
id, settle_stopped_report(
id, dev, policy.is_tracking_active(
id, dev.last_status)) ==
142 dev.optimistic.clear_position();
144 if (allow_tilt_from_extended_response &&
has_feature(dev.features, device_feature::TILT) &&
145 frame.data_len >= EXTENDED_TILT_RESPONSE_MIN_DATA_LEN &&
146 frame.data[EXTENDED_TILT_SELECTOR_OFFSET] == STATUS_TILT_SELECTOR) {
147 uint16_t
const tilt_raw = (frame.data[EXTENDED_TILT_MSB_OFFSET] << 8) | frame.data[EXTENDED_TILT_LSB_OFFSET];
149 dev.optimistic.clear_tilt();
157void track_motion_evidence(
IoDevice &dev,
bool moving, uint32_t now_ms) {
176std::optional<uint16_t> decode_remaining_time_s(
const IoFrame &frame,
bool trust_position) {
177 if (!trust_position || frame.data_len < PRIVATE_RESPONSE_REMAINING_TIME_MIN_DATA_LEN)
179 const uint16_t remaining = (frame.data[PRIVATE_RESPONSE_REMAINING_TIME_OFFSET] << 8) |
180 frame.data[PRIVATE_RESPONSE_REMAINING_TIME_OFFSET + 1];
181 if (remaining == PRIVATE_RESPONSE_REMAINING_TIME_NONE)
192uint32_t compute_private_response_delay_ms(
const IoDevice &dev, std::optional<uint16_t> remaining_s,
198 const uint32_t remaining_delay_ms =
199 remaining_s.has_value()
200 ? (*remaining_s * PRIVATE_RESPONSE_REMAINING_TIME_SCALE_MS) + PRIVATE_RESPONSE_REMAINING_TIME_BIAS_MS
204 return settle_delay_ms(policy.get_interval(
id), remaining_delay_ms,
false);
212uint32_t compute_status_update_delay_ms(
const IoDevice &dev,
const StatusPollPolicy &policy,
const std::string &
id) {
234bool read_status_byte(
const std::string &
id,
IoDevice &dev, uint8_t flags) {
239 ESP_LOGW(
detail::TAG,
"Device %s: status byte 0x%02X has no defined execution state: read by its stopped bit",
241 }
else if (state != dev.execution_state) {
245 dev.execution_state = state;
261 ESP_LOGD(
detail::TAG,
"Device %s: reports moving with no poll planned: following it",
id.c_str());
277 bool trust_position =
true) {
278 const bool reported_stopped = read_status_byte(
id, dev, frame.data[STATUS_STOPPED_FLAGS_OFFSET]);
279 dev.is_stopped = reported_stopped;
280 dev.last_status = millis();
281 if (trust_position) {
282 decode_status_fields(dev, frame, PRIVATE_RESPONSE_TARGET_OFFSET, PRIVATE_RESPONSE_CURRENT_OFFSET,
true,
id, policy);
298 track_motion_evidence(dev, !reported_stopped, dev.last_status);
305 std::optional<uint16_t> remaining_s = decode_remaining_time_s(frame, trust_position);
306 if (remaining_s.has_value() && *remaining_s != 0)
307 dev.reports_remaining_time =
true;
308 if (remaining_s.has_value() && *remaining_s == 0 && !dev.reports_remaining_time)
313 if (reported_stopped)
317 if (trust_position && remaining_s.has_value()) {
319 dev, dev.last_status + (*remaining_s * PRIVATE_RESPONSE_REMAINING_TIME_SCALE_MS) + PREDICTED_END_MARGIN_MS);
326 if (!follow_moving_report(
id, dev, policy))
331 policy.on_trusted_report(
id, remaining_s, dev.last_status);
335 uint32_t
const delay_ms =
336 std::max(compute_private_response_delay_ms(dev, remaining_s, policy,
id), policy.frozen_count_delay_ms(
id));
338 "Device %s: next status poll in %" PRIu32
" ms (remaining time=%s, configured interval=%" PRIu32
" ms)",
339 id.c_str(), delay_ms, remaining_s.has_value() ? std::to_string(*remaining_s).append(
"s").c_str() :
"none",
340 policy.get_interval(
id));
341 uint32_t
const new_deadline = dev.last_status + delay_ms;
342 uint32_t
const existing_deadline = policy.get_next_update(
id);
345 policy.set_next_update(
346 id, (existing_deadline != 0 && existing_deadline < new_deadline) ? existing_deadline : new_deadline);
354void apply_unsolicited_status_update(
const std::string &
id,
IoDevice &dev,
const IoFrame &frame,
356 const bool reported_stopped = read_status_byte(
id, dev, frame.data[STATUS_STOPPED_FLAGS_OFFSET]);
357 dev.is_stopped = reported_stopped;
358 dev.last_status = millis();
359 decode_status_fields(dev, frame, STATUS_UPDATE_TARGET_OFFSET, STATUS_UPDATE_CURRENT_OFFSET,
false,
id, policy);
361 track_motion_evidence(dev, !reported_stopped, dev.last_status);
367 if (!follow_moving_report(
id, dev, policy))
370 policy.set_next_update(
id, dev.last_status + compute_status_update_delay_ms(dev, policy,
id));
381void apply_heating_main_parameter(
const std::string &
id,
IoDevice &dev,
const IoFrame &frame) {
385 const auto current =
static_cast<uint16_t
>((frame.data[PRIVATE_RESPONSE_CURRENT_OFFSET] << BITS_PER_BYTE) |
386 frame.data[PRIVATE_RESPONSE_CURRENT_OFFSET + 1]);
387 dev.heating.main_parameter = current;
388 if ((current >> BITS_PER_BYTE) == profile->mp_prefix) {
389 const auto wire =
static_cast<int16_t
>(current & MAIN_PARAMETER_LOW_BYTE_MASK);
390 if (dev.heating.level != wire)
391 ESP_LOGI(
detail::TAG,
"Device %s: heating value 0x%02X",
id.c_str(),
static_cast<unsigned>(wire));
392 dev.heating.level = wire;
395 float celsius = 0.0F;
398 dev.heating.setpoint_c = celsius;
399 dev.heating.level = -1;
402 ESP_LOGD(
detail::TAG,
"Device %s: status main parameter 0x%04X is not a heating value",
id.c_str(), current);
416void apply_dhw_status_reply(
const std::string &
id,
IoDevice &dev,
const IoFrame &frame,
bool trust_position) {
421 if (dev.dhw.boost != (status.boost ? 1 : 0) || dev.dhw.away != (status.away ? 1 : 0)) {
422 ESP_LOGI(
detail::TAG,
"Device %s: hot water boost %s, away %s (FP15 0x%04X)",
id.c_str(),
423 status.boost ?
"on" :
"off", status.away ?
"on" :
"off",
static_cast<unsigned>(status.capabilities));
425 dev.dhw.capabilities = status.capabilities;
426 dev.dhw.boost = status.boost ? 1 : 0;
427 dev.dhw.away = status.away ? 1 : 0;
431 if (dev.dhw.target_c != celsius)
432 ESP_LOGI(
detail::TAG,
"Device %s: hot water target %.1f C",
id.c_str(),
static_cast<double>(celsius));
433 dev.dhw.target_c = celsius;
437 ESP_LOGD(
detail::TAG,
"Device %s: status main parameter 0x%02X%02X is not a hot water target",
id.c_str(),
438 frame.data[DHW_STATUS_MAIN_PARAMETER_OFFSET], frame.data[DHW_STATUS_MAIN_PARAMETER_OFFSET + 1]);
439 dev.dhw.target_c = NAN;
446void apply_battery_state(
const std::string &
id,
IoDevice &dev,
const IoFrame &frame) {
448 if (reported == dev.battery)
454 dev.battery = reported;
474 memset(dev.name, 0,
sizeof(dev.name));
476 memcpy(dev.name, name.c_str(), name.length());
494 ESP_LOGD(
detail::TAG,
"Device %s: no poll left to confirm its stop at %.2f%%, target %.2f%%: taken as at rest",
495 ended->c_str(),
static_cast<double>(dev->
position),
static_cast<double>(dev->
target));
500 if (dev->
position == UNKNOWN_POSITION) {
501 ESP_LOGI(
detail::TAG,
"Device %s: no stop reported since its move; shown at rest at an unknown position",
504 ESP_LOGI(
detail::TAG,
"Device %s: no stop reported since its move; shown at rest at %.0f%%", ended->c_str(),
505 static_cast<double>(dev->
position));
528 uint8_t node_id[NODE_ID_SIZE];
538 uint8_t node_id[NODE_ID_SIZE];
546 for (
const auto &device_id : device_ids) {
553 const std::string &controller, uint8_t cmd) {
557 const uint32_t now = millis();
561 ESP_LOGD(
detail::TAG,
"rx remote_activity device=%s controller=%s %s(0x%02X): scheduling status poll",
562 device_id.c_str(), controller.c_str(), what, cmd);
567 ESP_LOGD(
detail::TAG,
"rx remote_activity device=%s controller=%s %s(0x%02X): poll pending, pushed out",
568 device_id.c_str(), controller.c_str(), what, cmd);
572 ESP_LOGD(
detail::TAG,
"rx remote_activity device=%s controller=%s %s(0x%02X): %s, no poll", device_id.c_str(),
573 controller.c_str(), what, cmd,
575 ?
"answers a heard read"
576 :
"a read, and the device was read in the last 10 min");
584 if (frame.
cmd != CMD_CHALLENGE_REQ || memcmp(frame.
dst, this->node_id_, NODE_ID_SIZE) == 0)
597 if (this->
get_device(dst_id) !=
nullptr && memcmp(frame.
src, this->node_id_, NODE_ID_SIZE) != 0) {
606 if (linked ==
nullptr)
609 const std::vector<std::string> devices = *linked;
610 for (
const auto &device_id : devices)
616 const std::string &src_id)
const {
617 std::vector<std::string> devices;
619 devices = *id_linked;
623 for (
const auto &device_id : *class_linked) {
624 if (std::find(devices.begin(), devices.end(), device_id) == devices.end())
625 devices.push_back(device_id);
640 if (!target_devices.empty() && this->tuning_.remote_activity_catchup_poll_ms != 0)
645 const std::vector<std::string> &device_ids) {
649 const bool is_stop = info.
main0 == POS_STOP;
652 for (
const auto &device_id : device_ids) {
664 }
else if (target.has_value()) {
676 const std::string &src_id) {
684 if (!this->is_connected()) {
685 ESP_LOGD(
detail::TAG,
"1W sender %s has intent but the API is not connected, skipping %s", src_id.c_str(),
690 ESP_LOGD(
detail::TAG,
"1W sender %s has intent but is not in exposed_senders, skipping %s", src_id.c_str(),
706 if (frame.
data_len < STATUS_UPDATE_MIN_DATA_LEN) {
720 apply_unsolicited_status_update(
id, dev, frame, this->
poll_policy_);
725 apply_battery_state(
id, dev, frame);
738 if (!originator.empty())
739 ESP_LOGD(
detail::TAG,
"Device %s: status update originator=%s",
id.c_str(), originator.c_str());
748 if (device_ptr ==
nullptr) {
755 if (frame.
cmd == CMD_PRIVATE_RESP) {
756 if (frame.
data_len < PRIVATE_RESPONSE_MIN_DATA_LEN) {
763 apply_dhw_status_reply(
id, dev, frame, trust_position);
776 apply_private_response_status(
id, dev, frame, this->
poll_policy_, trust_position);
781 if (trust_position) {
786 apply_battery_state(
id, dev, frame);
787 apply_heating_main_parameter(
id, dev, frame);
796 if (frame.
cmd == CMD_STATUS_UPDATE) {
801 if (frame.
cmd == CMD_GET_NAME_RESP) {
802 if (frame.
data_len < GET_NAME_RESPONSE_MIN_DATA_LEN) {
807 apply_name_response(dev, frame);
808 ESP_LOGI(
detail::TAG,
"Device %s: name=%s",
id.c_str(), dev.
name[0] ==
'\0' ?
"" : dev.
name);
813 if (frame.
cmd == CMD_GET_INFO2_RESP) {
814 if (frame.
data_len < GET_INFO2_RESPONSE_MIN_DATA_LEN) {
822 apply_info2_response(dev, frame);
823 ESP_LOGI(
detail::TAG,
"Device %s: type=%s (%u) class=%s profile=%s subtype=%u",
id.c_str(),
828 "Device %s: type learned at runtime, not declared in YAML — add `%s` to skip "
829 "re-learning it on every future boot",
835 if (frame.
cmd == CMD_ERROR_RESP) {
836 if (frame.
data_len < ERROR_RESPONSE_MIN_DATA_LEN) {
868 if (frame.
cmd == CMD_LIMITATION_STATUS_REQ) {
870 }
else if (frame.
cmd == CMD_LIMITATION_STATUS_RESP) {
876constexpr size_t DEVICE_EVENT_HEAD_BUFFER_SIZE = 48;
877constexpr size_t DEVICE_EVENT_CMD_BUFFER_SIZE = 8;
878constexpr uint32_t DEVICE_EVENT_MS_PER_SECOND = 1000;
900 for (uint8_t i = 0; i < frame.
data_len; i++) {
901 snprintf(byte_hex,
sizeof(byte_hex), i == 0 ?
"%02X" :
" %02X", frame.
data[i]);
908 ESP_LOGI(
detail::TAG,
"Device event from %s: cmd=0x%02X dst=%s payload=%s", src_id.c_str(), frame.
cmd,
909 dst_id.c_str(), payload.c_str());
911 ESP_LOGD(
detail::TAG,
"Device event from %s: cmd=0x%02X dst=%s payload=%s", src_id.c_str(), frame.
cmd,
912 dst_id.c_str(), payload.c_str());
914 char head[DEVICE_EVENT_HEAD_BUFFER_SIZE];
916 millis() / DEVICE_EVENT_MS_PER_SECOND);
917 char cmd_hex[DEVICE_EVENT_CMD_BUFFER_SIZE];
918 snprintf(cmd_hex,
sizeof(cmd_hex),
" 0x%02X", frame.
cmd);
919 this->
last_device_event_ = std::string(head) + src_id + cmd_hex + (payload.empty() ?
"" :
" ") + payload;
958 if ((frame.
ctrl0 & CTRL0_PROTOCOL_1W) != 0) {
960 const uint32_t now = millis();
989 memcpy(incoming.
dst, frame.
dst, NODE_ID_SIZE);
1006 if (frame.
cmd == CMD_STATUS_UPDATE && memcmp(frame.
dst, this->node_id_, NODE_ID_SIZE) == 0) {
1030 if (frame.
cmd == CMD_PRIVATE_RESP || frame.
cmd == CMD_STATUS_UPDATE) {
1062 const bool addressed_to_us = memcmp(frame.
dst, this->node_id_, NODE_ID_SIZE) == 0;
1063 if (addressed_to_us && std::strcmp(
command_name(frame.
cmd),
"UNKNOWN_CMD") == 0) {
1066 "Received unknown command 0x%02X from %s. "
1067 "If you see this repeatedly, please file a GitHub issue with this command ID, "
1068 "your device model, and the log context so protocol support can be extended.",
1069 frame.
cmd, src_id.c_str());
const std::vector< std::string > * linked_devices(const std::string &remote_id) const
Retrieve the list of device IDs linked to a remote.
const std::vector< std::string > * linked_devices_for_class(DeviceType type) const
Retrieve the list of device IDs linked to a device class.
bool apply_optimistic_target(const std::string &device_id, float target_io_position)
Set an optimistic target position ahead of a confirming poll/response, and notify.
bool apply_optimistic_stop(const std::string &device_id, bool restorable=false)
Predict that a device has stopped (e.g.
IoDevice * get(const std::string &device_id)
Retrieve a registered device by ID.
void maybe_fire_sender_event_(const OneWayFrameInfo &info, bool linked, const std::string &src_id)
Fire the sender HA event for a decoded 1W frame, if the sender is exposed.
uint32_t last_1w_activity_ms_
millis() of the most recent 1W frame of any kind, including ones dropped as duplicates — a repeat sti...
uint32_t device_event_count_
Device events recorded since boot, the last one's text and its subscriber; see record_device_event_()...
bool record_probe_reply_event(const IoFrame &reply)
Record a diagnostic probe's reply on the "Last Device Event" sensor when it is a device event report ...
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 follow_linked_remote_press_(const OneWayFrameInfo &info, const std::string &src_id)
React to a deduplicated 1W press from a remote: apply the optimistic state to the devices it controls...
void note_status_reported_(const std::string &device_id)
The device just reported its status: move its status refresh (if it has one) a whole interval out.
virtual void queue_request_device_status(const std::string &device_id)
Queue an async status request; returns immediately, executed in loop().
void record_oneway_pairing_gesture_(const IoFrame &frame, uint32_t now)
If frame matches a 1W remote's pairing gesture (decisions::is_one_way_pairing_gesture()),...
TuningConfig tuning_
Runtime tuning overrides.
std::vector< std::string > resolve_1w_target_devices_(const OneWayFrameInfo &info, const std::string &src_id) const
Resolve the set of devices a 1W frame should affect: devices linked to the sending remote by node ID,...
StatusPollPolicy poll_policy_
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 diagnostic_probes_enabled_
Whether diagnostic probes (DiagnosticProbes::probe_device()/probe_sweep()) are enabled.
void update_device_status_(const IoFrame &frame, bool trust_position=true)
Extract supported position or metadata info from a response frame and merge it into the device record...
RecentOneWayPairingSighting recent_oneway_pairing_sighting_
Most recent 1W pairing-gesture frame seen on the hub's normal passive RX path (e.g.
void schedule_device_polls_(const std::vector< std::string > &device_ids, uint32_t delay_ms)
Schedule status polls for a fixed list of devices (shared by the id-linked and class-linked 1W paths)...
void record_device_event_(const IoFrame &frame)
Record a device event report, or a frame from a registered device that nothing else here handles: log...
void settle_ended_status_tracking_()
Settle every device whose polling window ran out (StatusPollPolicy::pop_ended_device()).
void record_1w_activity_(uint32_t now)
Record that a 1W frame just went out on the radio — ours or someone else's — updating last_1w_activit...
std::function< void()> device_event_callback_
void act_on_foreign_frame_(const std::string &device_id, ForeignFrameKind kind, const std::string &controller, uint8_t cmd)
Decide, and act on, what a frame of another controller means for one registered device: a heard write...
void apply_status_update_(const std::string &id, IoDevice &dev, const IoFrame &frame)
Handle an authenticated CMD_STATUS_UPDATE from a registered device: a device event report (decisions:...
std::string last_device_event_
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 schedule_heating_poll_(const std::string &device_id, uint32_t delay_ms)
Arm the heater's regular-cadence poll timer.
OnewayKeyAdoption oneway_key_adoption_
Opt-in, receive-only 1W controller-key adoption listener (oneway_key_adoption.cpp).
void notify_device_update_(const std::string &id)
Fire all registered device update callbacks for the given device ID.
decisions::OneWayDedupState last_1w_logged_
Identity of the last processed 1W frame, for burst suppression; see decisions::is_duplicate_1w_frame(...
KeyExtractionResponder key_extraction_
Device-role responder for the "Recover System Key" feature (key_extraction_responder....
uint8_t node_id_[NODE_ID_SIZE]
std::vector< std::string > exposed_senders_
1W sender node IDs (remotes or sensors) allowed to fire the sender HA event (add_exposed_sender).
bool act_on_overheard_request_(const IoFrame &frame)
Hand another controller's frame to act_on_foreign_frame_(): per the registered device it is addressed...
void process_received_packet_(const RadioRxPacket &packet)
Parse a received frame, merge supported device state or metadata, and notify callbacks.
bool authenticate_request_(const IoFrame &request, uint32_t freq)
Handle an inbound authenticated command from a device (status updates, etc.).
uint32_t first_1w_activity_ms_
millis() of the first 1W frame in the current burst.
bool apply_optimistic_linked_state_(const OneWayFrameInfo &info, const std::vector< std::string > &device_ids)
Apply optimistic target state to every device in device_ids, when the decoded frame carries a resolva...
void note_foreign_challenge_(const IoFrame &frame)
Hand a registered device's challenge (0x3C) to another controller to act_on_foreign_frame_().
bool record_if_device_event_(const IoFrame &frame, bool reached_unhandled_fallback)
Record frame as a device event when it qualifies: its source is a registered device and it is either ...
void try_adopt(const IoFrame &frame)
Decode an inbound CMD_ONEWAY_ADD_CONTROLLER (0x30) while armed, report the result,...
void record_observed_class(const OneWayFrameInfo &info)
Remember the most recent 1W target device class observed from info.src, for the adoption report's io_...
virtual uint16_t response_preamble() const
Return the preamble length for response/continuation frames.
const RadioCaptureInfo & get_last_capture() const
Get the most recent radio capture info.
Per-hub poll scheduling and failure-backoff policy.
ForeignAction on_foreign_frame(const std::string &device_id, ForeignFrameKind kind, const std::string &controller, uint8_t cmd, uint32_t now, uint32_t last_status_ms)
Decide what an overheard frame of another controller means for this device.
void on_remote_poll_ran(const std::string &device_id)
The poll armed by foreign traffic ran (or was dropped): nothing is pending any more.
void on_linked_remote_activity()
A linked remote was heard: open a burst, or keep the open one.
void begin_tracking(const std::string &device_id, uint32_t initial_delay_ms, uint32_t now)
Begin bounded follow-up polling after a command or remote activity.
std::optional< std::string > pop_ended_device()
Return and consume a device whose polling window ended without the device being reported at rest: the...
Pure transition helpers for hub-owned exchange and pairing frame decisions.
Helpers shared only by the hub's own implementation files (hub_*.cpp).
bool stopped_report_at_rest(StoppedReport report)
True when a verdict means the device is at rest.
bool is_one_way_pairing_gesture(bool oneway, const uint8_t dst[NODE_ID_SIZE], uint8_t cmd)
True if a frame's shape matches a 1W remote's pairing gesture (issue #27/#65): CTRL0 1W bit set,...
bool is_duplicate_1w_frame(const OneWayDedupState &last, const OneWayDedupState &incoming, uint32_t window_ms)
Decide whether an incoming 1W frame repeats the previous one inside the burst window.
bool is_exchange_internal_command(uint8_t cmd)
Returns true for commands that are internal to an exchange handshake and carry no useful information ...
StoppedReport classify_stopped_report(const StoppedReportInput &in)
Decide whether a "stopped" report means at rest.
bool stopped_report_keeps_candidate(StoppedReport report)
True when a verdict leaves an off-target report to remember for the next one.
bool is_device_event_report(const IoFrame &frame)
Returns true for a frame that reports a device event rather than a status: a CMD_SET_CONFIG1_RESP,...
bool oneway_burst_started_fresh(uint32_t last_1w_activity_ms, uint32_t now, uint32_t quiet_ms)
Whether a 1W frame arriving at now starts a new burst rather than extending the current one — true if...
uint32_t heating_poll_timer_id(const uint8_t node_id[NODE_ID_SIZE])
Numeric set_timeout() id for a heater's regular-cadence poll timer (IOHomeControlComponent::schedule_...
std::array< uint32_t, 3 > status_update_ack_channel_order(uint32_t receive_freq_hz)
The order in which a 0x72 status-update acknowledgement is sent on the three channels: the channel th...
StoppedReport
What the hub makes of one status report's movement state.
@ SETTLING
Stopped off its target for the first time: treated as moving until the next report confirms it.
@ STOPPED_UNCONFIRMABLE
Stopped off its target and no follow-up poll will run, so the device's own flag stands.
@ STOPPED_CONFIRMED
Stopped off its target, as in the remembered report: at rest there.
bool reads_stopped_by_status_byte(uint8_t status_byte)
Does byte 0 of a status reply say the device is at rest?
uint32_t remote_poll_timer_id(const uint8_t node_id[NODE_ID_SIZE])
Numeric set_timeout() id for a device's remote-activity poll timer (hub_status.cpp's schedule_status_...
std::string describe_limitation_reply(const IoFrame &frame)
Render a CMD_LIMITATION_STATUS_RESP frame for a log line or probe report.
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.
void log_frame_issue(IOHomeControlComponent *component, const uint8_t *own_node_id, const char *direction, const char *reason, const IoFrame &frame, uint8_t len)
Log a frame‑level issue (unregistered endpoints, unsupported commands).
std::string describe_learned_device_type(DeviceType type)
Build the YAML line to add once a device's type is learned at runtime.
void apply_last_command_record(IoDevice &dev, const LastCommandRecord &record)
Store a decoded record on the device, if it is valid.
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 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 stop_candidate_matches_observation(const IoDevice &dev)
True when the remembered off-target stop is the device's last trusted report.
std::string describe_status_update_originator(const IoFrame &frame)
Describe a 0x71 status update's Command Originator as "name(0xXX)".
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.
std::string describe_limitation_request(const IoFrame &frame)
Render a CMD_LIMITATION_STATUS_REQ frame for a log line.
bool is_settling(const IoDevice &dev)
True while a device that reported stopped off its target waits for the confirming poll.
bool known_device_reports_motion_through_status(const IoDevice &dev)
Does the device report its movement through status replies?
void normalize_stopped_state(IoDevice &dev)
Normalize the stopped flag of an execute ack against the last observed target and position.
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.
void clear_command_result(IoDevice &dev)
Clear a previously recorded CMD_ERROR_RESP result, if any.
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.
void update_rain_limitation(const std::string &id, IoDevice &dev, float predicted_target, bool record_in_reply, uint32_t now_ms)
Refresh the derived rain-limitation flag from a status reply the device just sent.
ExecutionState execution_state_of(uint8_t status_byte)
Read the execution state out of byte 0 of a status reply.
const HeatingProfile * find_heating_profile(DeviceType type, uint8_t subtype, DeviceModel model)
Look up the heating profile for a (type, subtype, model).
ForeignFrameKind
What an overheard frame of another controller is.
@ REQUEST_TO_DEVICE
Another controller's command to a registered device.
@ DEVICE_CHALLENGE_TO_OTHER
A registered device's challenge (0x3C) addressed to another controller.
bool decode_temperature_main_parameter(uint16_t main_parameter, float min_c, float max_c, float &celsius)
Decode a main parameter as a temperature over an encoding range (the inverse of encode_temperature_ma...
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
void resolve_device_features(IoDevice &dev)
Resolve a device's capability bits and inversion from its kind and its YAML overrides.
const char * execution_state_name(ExecutionState state)
Get a human-readable name for an ExecutionState.
const char * command_name(uint8_t cmd)
Get a human-readable name for any IO-Homecontrol command ID.
const char * device_type_name(DeviceType type)
Convert a DeviceType to a lowercase string identifier.
@ UNKNOWN
Unknown/unspecified device.
@ TEMPERATURE_EXECUTE
Standard EXECUTE with a temperature or an alias as the main parameter; read with 0x03.
@ PRIVATE_REGISTERS
CMD_WRITE_PRIVATE register reads and writes (proto_heating.h).
LastCommandRecord decode_last_command_record(const IoFrame &frame, uint8_t base)
Decode the last-command record at base from a status-bearing payload.
float decode_tilt_report(uint16_t tilt_raw)
Decode tilt angle from raw 16‑bit value.
const char * device_operation_profile_name(DeviceType type, uint8_t subtype)
Human‑readable operation profile name for a device kind.
constexpr bool has_feature(DeviceFeatures set, DeviceFeatures bit)
True when every bit of bit is set in set.
OneWayFrameInfo decode_1w_frame(const IoFrame &frame)
Decode a parsed 1W frame into a structured OneWayFrameInfo.
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 the 10-bit node type from two packed metadata bytes.
BatteryState
A device's coarse battery level, as it reports it in byte 1 of its status payload.
bool decode_dhw_operating_status(const IoFrame &reply, DhwOperatingStatus &status)
Capabilities and operating-mode state from the reply to the status read DHW_STATUS_FPI1 / DHW_STATUS_...
bool parse(const uint8_t *buf, uint8_t buf_len, IoFrame &f)
Parse a wire buffer into a parsed IoFrame (validates length and CTRL0).
constexpr uint8_t STATUS_UPDATE_EVENT_DATA_OFFSET
First event-data byte.
const char * device_capability_class_name(DeviceType type)
Get a human‑readable name for a capability class.
bool effective_is_stopped(const IoDevice &dev)
Whether a consumer should treat the device as at rest, prediction first.
ExecutionState
What a device says it is doing, from bits [2:0] of byte 0 of a status reply (CMD_PRIVATE_RESP,...
@ UNDEFINED
Bits [2:0] hold 6 or 7, which no source defines.
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 void log_overheard_limitation_read(const IoFrame &frame)
Log an overheard limitation request/reply decoded at DEBUG; any other frame is ignored.
void note_moving_evidence(IoDevice &dev, uint32_t now_ms)
Record that a device is (believed to be) travelling right now.
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.
void note_moving_until(IoDevice &dev, uint32_t until_ms)
Record when the travel the device is reporting should be over, from its own remaining time.
uint8_t decode_packed_device_subtype(uint8_t type_subtype)
Decode a protocol-packed device subtype from the second metadata byte.
uint32_t settle_delay_ms(uint32_t interval_ms, uint32_t remaining_delay_ms, bool cap_for_stop)
Resolve the follow-up settle-poll delay while a device may still be moving.
@ BROADCAST_TYPE
Broadcast to specific device type with non-standard suffix.
bool device_has_no_motion(DeviceType type, uint8_t subtype)
Does this device never move, so a status reply must not start the movement settle loop?
MovingReportAction
What the hub does with a status report that reads as moving.
@ FOLLOW
A window is open: schedule the next poll as usual.
@ OPEN
No window: one was opened now; schedule the next poll as usual.
BatteryState decode_battery_state(uint8_t status_byte)
Decode the coarse battery level from byte 1 of a status payload (CMD_PRIVATE_RESP to a status poll,...
const char * battery_state_name(BatteryState state)
Get the name a battery level is published under.
bool decode_dhw_status_target(const IoFrame &reply, float &celsius)
Target temperature from a status reply (CMD_PRIVATE_RESP) of a domestic hot water heater.
constexpr uint8_t PRIVATE_RESPONSE_LAST_COMMAND_OFFSET
Offset of the last-command record within each status-bearing payload.
void clear_moving_evidence(IoDevice &dev)
Forget that a device was travelling: it reported itself stopped (including in the ack to a STOP),...
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 create_status_update_resp(IoFrame &f, const uint8_t *own, const uint8_t *dst)
Build a status-update acknowledgment (0x72).
bool hex_to_bytes(const std::string &hex, uint8_t *out, uint8_t len)
Convert a hex string (e.g., "123ABC") to a byte array.
@ PUSH_PENDING
A poll armed by foreign traffic is pending: move its timer out, change nothing else.
@ ARM
Schedule a poll REMOTE_ACTIVITY_STATUS_POLL_DELAY_MS from now and begin tracking.
@ IGNORE
Nothing: no poll, no tracking change.
Command builders for the IO‑Homecontrol protocol.
Pure codec for a domestic hot water heater: its family-0x00 registers and its target temperature.
Heating profiles: what a climate card offers for each kind of heater, and how.
The operating-mode part of the status read DHW_STATUS_FPI1 / DHW_STATUS_FPI2 (its main parameter,...
Runtime state of a paired IO‑Homecontrol device.
float target
Target position the device is moving toward.
DeviceFeatures features
What this device can do, resolved by resolve_device_features().
ExecutionState execution_state
Execution state of the last status report that carried one; UNDEFINED until then, and for a device th...
uint32_t last_status
millis() timestamp of last received status.
char name[DEVICE_NAME_BUFFER_SIZE]
Cached UTF-8 device name decoded from Latin-1 wire payloads.
OptimisticState optimistic
Hub-side predictions; see OptimisticState. Never observation.
float position
Current position: 0=open, 100=closed, or UNKNOWN_POSITION.
uint8_t subtype
Device subtype (manufacturer‑specific).
DeviceType type
Device type (shutter, awning, etc.).
bool is_stopped
True if device is not moving.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
uint8_t data[FRAME_MAX_DATA_SIZE]
Command parameters (0–23 bytes). Never includes mac.
uint8_t ctrl0
Control byte 0: flags + length.
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.
One decoded last-command record.
bool valid
False when the payload was too short, or the record was unpopulated.
Decoded representation of a 1W remote frame.
bool has_intent
True if originator/ACEI/intent fields were decoded.
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 main1
Raw second main byte (has_intent only); feeds oneway_intent_to_target().
AddressClass address_class
Classification of the broadcast address.
void clear_position()
Withdraw the position prediction; call when a decoded position observation supersedes it.
float target
Predicted main-position target, or UNKNOWN_POSITION.
int16_t rssi_dbm
Received signal strength (dBm).
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.
uint8_t dst[NODE_ID_SIZE]
Destination node ID (the 1W broadcast address, in practice).
uint8_t src[NODE_ID_SIZE]
Sender node ID.
uint8_t cmd
Frame command byte.
uint32_t seen_ms
millis() the frame was seen; 0 if none seen since boot.
int16_t rssi
RSSI in dBm at the time it was overheard.
uint32_t remote_activity_poll_delay_ms
Delay from a linked-remote press (not a STOP) to the first status poll of its devices.
Key fields of the last processed 1W frame, used to collapse a remote's repeat burst.
uint8_t dst[NODE_ID_SIZE]
Destination address; compared only when has_intent is false.