36constexpr size_t EXECUTE_ACTION_BUF_SIZE = 40;
40constexpr uint8_t FORCE_OPEN_WIRE_POSITION = 0;
41constexpr uint8_t FORCE_OPEN_WIRE_POSITION_INVERTED = 100;
47const char *position_command_action(
const IoDevice &dev, uint8_t position) {
48 if (position == POS_STOP)
52 return "set position";
54 bool const active_state = position == BINARY_ENTITY_ON_POSITION;
59 return active_state ?
"turn on" :
"turn off";
61 return active_state ?
"unlock" :
"lock";
68 return active_state ?
"open" :
"close";
77const char *operation_profile_name(
const IoDevice &dev) {
78 if (
has_feature(dev.features, device_feature::LEVELS) &&
80 return "dimmable_light";
89const char *position_rejection_profile(
const IoDevice &dev, uint8_t position) {
96 if (!
has_feature(dev.features, device_feature::LEVELS) && position != POS_STOP &&
98 return "cover with intermediate positions (LEVELS feature)";
113 bool (*accepts)(
const IoDevice &dev);
114 const char *rejection_noun;
115 const char *expected;
118constexpr QueueGuard QUEUE_GUARD_COVER{
120 "queued cover command",
"cover entity"};
122 "queued tilt command",
"tilt-capable cover"};
123constexpr QueueGuard QUEUE_GUARD_POSITION_AND_TILT{
125 "tilt-capable cover"};
126constexpr QueueGuard QUEUE_GUARD_LIGHT{
128 "queued light command",
"light entity"};
129constexpr QueueGuard QUEUE_GUARD_FAN{
131 "queued fan command",
"exterior heating (fan entity)"};
132constexpr QueueGuard QUEUE_GUARD_LOCK{
134 "queued lock command",
"lock entity"};
135constexpr QueueGuard QUEUE_GUARD_SWITCH{
137 "queued switch command",
"switch entity"};
138constexpr QueueGuard QUEUE_GUARD_STATUS{
140 "status-capable actuator"};
146bool queue_guard_rejects(
IOHomeControlComponent *hub,
const std::string &device_id,
const QueueGuard &guard) {
147 const IoDevice *dev = hub->get_device(device_id);
148 if (dev !=
nullptr && !guard.accepts(*dev)) {
161 if (existing != 0 && existing < millis() + delay_ms)
213 const bool unconfirmed_counts_as_success = request.
cmd == CMD_EXECUTE;
234 if (dbg.saw_challenge)
238 if (retry_after_fail_ms != 0) {
250 if (warn_on_no_response) {
251 ESP_LOGW(
detail::TAG,
"Command 0x%02X failed for device %s: no valid response (stage=%s tries=%u)", request.
cmd,
252 device_id.c_str(), dbg.stage, dbg.tries);
271 if (retry_after_fail_ms != 0)
280 if (response.
cmd == CMD_ERROR_RESP) {
285 if (retry_after_fail_ms != 0)
293 bool warn_on_no_response, uint32_t retry_after_fail_ms,
297 this->
exchange_and_record_(device_id, request, response, {warn_on_no_response, retry_after_fail_ms, attempts})) {
316 const IoFrame &response, uint32_t retry_after_fail_ms) {
325 }
else if (dev !=
nullptr) {
332 if (retry_after_fail_ms != 0)
338 const std::function<
bool(
const IoDevice &)> &accepts,
339 const char *rejection_profile,
353 }
else if (accepted) {
367 const std::function<
bool(
const IoDevice &)> &accepts,
368 const char *rejection_profile,
377 if (!accepts(*dev)) {
384 ESP_LOGI(
detail::TAG,
"Sending %s to device %s (profile=%s)", spec.
action, device_id.c_str(),
385 operation_profile_name(*dev));
388 if (!build(request, *dev)) {
406 const auto *dev = this->
get_device(device_id);
412 device_id, {position_command_action(*dev, position), position == POS_STOP},
414 position_rejection_profile(*dev, position),
417 if (!
has_feature(d.features, device_feature::EXTENDED_POSITION_FRAME) ||
430 char expected[EXECUTE_ACTION_BUF_SIZE] =
"cover entity";
431 if (named !=
nullptr)
432 snprintf(expected,
sizeof(expected),
"cover with %s position", named->
name);
448 has_feature(d.features, device_feature::EXTENDED_POSITION_FRAME));
450 const uint8_t open_position = d.inverted ? FORCE_OPEN_WIRE_POSITION_INVERTED : FORCE_OPEN_WIRE_POSITION;
451 return has_feature(d.features, device_feature::EXTENDED_POSITION_FRAME)
459 char action[EXECUTE_ACTION_BUF_SIZE];
460 snprintf(action,
sizeof(action),
"tilt=%u%%", tilt_percent);
463 "tilt-capable cover",
470 uint8_t tilt_percent) {
471 char action[EXECUTE_ACTION_BUF_SIZE];
472 snprintf(action,
sizeof(action),
"position=%u%% tilt=%u%%", position, tilt_percent);
475 "tilt-capable cover",
488 bool scheduler_managed,
bool state_unknown) {
489 if (!scheduler_managed) {
491 return state_unknown ? attempts.with_state_unknown() : attempts;
497 this->poll_policy_.take_settling_read(device_id)));
504 bool scheduler_managed) {
505 if (!scheduler_managed)
506 return EXCHANGE_RETRY_COUNT;
508 this->poll_policy_.get_auth_poll_failures(device_id));
530 if (
has_feature(dev->features, device_feature::WATER_HEATER))
536 bool const request_ok =
has_feature(dev->features, device_feature::TILT)
546 const uint32_t retry_after_fail_ms = scheduler_managed ? STATUS_RETRY_AFTER_FAIL_MS : 0;
551 if (heating !=
nullptr) {
554 this->
schedule_heating_poll_(device_id, configured_ms != 0 ? configured_ms : HEATING_POLL_DEFAULT_INTERVAL_MS);
565bool count_poll_miss(
PeriodicPollPolicy &policy,
const std::string &device_id,
bool unanswered,
const char *label) {
570 ESP_LOGD(
detail::TAG,
"Device %s: %s poll unanswered, asking again in %" PRIu32
" s", device_id.c_str(), label,
571 PERIODIC_POLL_MISS_RETRY_MS / 1000);
586 ESP_LOGD(
detail::TAG,
"Device %s: rain sensor poll deferred, device is moving", device_id.c_str());
604 std::string limit_text;
608 uint8_t raw[FRAME_MAX_SIZE] = {0};
609 const uint8_t raw_len =
serialize(response, raw,
sizeof(raw));
617 ESP_LOGD(
detail::TAG,
"Device %s: unexpected reply 0x%02X to the limitation read", device_id.c_str(),
621 ESP_LOGW(
detail::TAG,
"Device %s answers the limitation read with an error; its rain sensor stays unknown",
630 const std::string &limit_text,
bool unanswered) {
638 }
else if (count_poll_miss(this->
rain_poll_policy_, device_id, unanswered,
"rain sensor")) {
642 ESP_LOGI(
detail::TAG,
"Device %s: rain sensor %s -> %s%s%s%s", device_id.c_str(),
644 limit_text.empty() ?
"" :
" (minimum limit ", limit_text.c_str(), limit_text.empty() ?
"" :
")");
666std::optional<uint16_t> &event_reading_field(
IoDevice &dev,
bool sun) {
671std::optional<uint16_t> decode_event_reading(
bool sun,
const IoFrame &response) {
681std::string format_event_reading(
const std::optional<uint16_t> &reading,
bool sun) {
682 if (!reading.has_value())
684 return std::to_string(*reading) + (sun ?
"" :
" %");
697 const char *
const label = sun ?
"sun level" :
"battery level";
702 ESP_LOGD(
detail::TAG,
"Device %s: %s poll deferred, device is moving", device_id.c_str(), label);
703 policy.
defer(device_id, PERIODIC_POLL_MOVING_RETRY_MS, millis());
709 sun ? EVENT_LOCAL_SENSOR : EVENT_BATTERY_LEVEL_CHANGE, dev->
low_power))
722 std::optional<uint16_t> reading;
726 uint8_t raw[FRAME_MAX_SIZE] = {0};
727 const uint8_t raw_len =
serialize(response, raw,
sizeof(raw));
729 reading = decode_event_reading(sun, response);
734 "Device %s answers the %s poll with cmd=0x%02X payload=%s, a shape not decoded; its %s "
736 device_id.c_str(), label, response.
cmd, hex, label);
739 ESP_LOGW(
detail::TAG,
"Device %s refuses the %s subscription; its %s stays unknown", device_id.c_str(), label,
744 return reading.has_value();
748 const std::optional<uint16_t> &reading,
bool unanswered) {
754 std::optional<uint16_t> &field = event_reading_field(*dev, sun);
755 const std::optional<uint16_t> before = field;
756 const char *
const label = sun ?
"sun level" :
"battery level";
757 if (reading.has_value()) {
760 }
else if (count_poll_miss(policy, device_id, unanswered, label)) {
763 if (field != before) {
764 ESP_LOGI(
detail::TAG,
"Device %s: %s %s -> %s", device_id.c_str(), label, format_event_reading(before, sun).c_str(),
765 format_event_reading(field, sun).c_str());
766 }
else if (reading.has_value()) {
767 ESP_LOGD(
detail::TAG,
"Device %s: %s %s (unchanged)", device_id.c_str(), label,
768 format_event_reading(reading, sun).c_str());
816 return this->
set_light_position(device_id, on ? BINARY_ENTITY_ON_POSITION : BINARY_ENTITY_OFF_POSITION);
830 return this->
set_device_position(device_id, on ? BINARY_ENTITY_ON_POSITION : BINARY_ENTITY_OFF_POSITION);
845 return this->
set_device_position(device_id, locked ? BINARY_ENTITY_OFF_POSITION : BINARY_ENTITY_ON_POSITION);
852 device_id, {
"heating level",
false},
862 device_id, {
"heating temperature",
false},
864 "temperature-controlled climate device",
869 main_parameter, fp.
value);
889 IoFrame &response, uint8_t max_tries) {
893 const char *direction = is_read ?
"read" :
"write";
894 ESP_LOGI(
detail::TAG,
"Sending heating %s %s to device %s", direction, label, device_id.c_str());
898 const bool acknowledged = got_reply && response.
cmd == CMD_WRITE_PRIVATE_ACK;
915 if (got_reply && response.
cmd == CMD_ERROR_RESP && response.
data_len > 0) {
917 }
else if (acknowledged) {
925 ESP_LOGW(
detail::TAG,
"Heating %s %s: no CMD_WRITE_PRIVATE_ACK from device %s", direction, label,
934 ESP_LOGD(
detail::TAG,
"heating %s %s ACK payload: %s", direction, label, ack_payload_hex);
951 if (payload_len == 0) {
952 ESP_LOGW(
detail::TAG,
"Heating command %s for device %s rejected: value %.2f out of range",
975 const char *label,
IoFrame &response, uint8_t max_tries) {
990 ESP_LOGW(
detail::TAG,
"Heating read %s for device %s: failed to build frame", label, device_id.c_str());
999 ESP_LOGD(
detail::TAG,
"heating %s reply from device %s is not for register 0x%02X%02X: %s", label,
1000 device_id.c_str(), reg_hi, reg_lo, payload_hex);
1007 uint8_t max_tries) {
1017 uint8_t register_low = 0;
1026 response, max_tries))
1034 device_id.c_str(), payload_hex);
1043 dev->heating.store(fn, value);
1048 if (value_out !=
nullptr)
1069 bool mode_has_value = mode_answered;
1070 uint8_t mode_register = 0;
1074 mode_reply, max_tries);
1078 if (!name_read || !mode_answered)
1087 ESP_LOGI(
detail::TAG,
"Device %s reports model name \"%s\"", device_id.c_str(),
1088 name.empty() ?
"(none)" : name.c_str());
1091 const char *towel_name = towel !=
nullptr ? towel->
name :
"towel_dryer";
1095 "Device %s is declared with io_model: %s, but it holds no towel-dryer mode (register 0x0136 is "
1096 "empty): it looks like a radiator. Remove io_model: from its climate entry",
1097 device_id.c_str(), towel_name);
1101 "Device %s holds a towel-dryer mode (register 0x0136): it looks like a towel dryer. Set "
1102 "io_model: %s on its climate entry for its modes, Boost and Drying",
1103 device_id.c_str(), towel_name);
1111 uint8_t max_tries) {
1143 this->poll_policy_.get_auth_poll_failures(device_id));
1146 size_t answered = 0;
1192 this->
schedule_heating_poll_(device_id, configured_ms != 0 ? configured_ms : HEATING_POLL_DEFAULT_INTERVAL_MS);
1199 IoFrame &response, uint8_t max_tries) {
1208 return this->
read_private_register_(device_id, DHW_REGISTER_HIGH_BYTE, index, label, response, max_tries);
1214const char *dhw_register_label(uint8_t index) {
1216 case DHW_REG_WATER_TEMPERATURE:
1217 return "dhw_water_temperature";
1220 case DHW_REG_HEATING:
1221 return "dhw_heating";
1222 case DHW_REG_BOOST_DURATION:
1223 return "dhw_boost_duration";
1224 case DHW_REG_AWAY_DURATION:
1225 return "dhw_away_duration";
1226 case DHW_REG_ENERGY:
1227 return "dhw_energy";
1231 return "dhw_register";
1236bool store_dhw_duration(
const std::string &device_id, DhwReadings &readings, uint8_t index,
const uint8_t *value,
1238 uint8_t duration = 0;
1241 const bool boost = index == DHW_REG_BOOST_DURATION;
1242 (boost ? readings.boost_duration : readings.away_duration) = duration;
1243 ESP_LOGD(
detail::TAG,
"Device %s: hot water %s duration %u", device_id.c_str(), boost ?
"boost" :
"away",
1244 static_cast<unsigned>(duration));
1249bool store_dhw_brand(
const std::string &device_id,
DhwReadings &readings,
const uint8_t *value,
size_t len) {
1252 if (readings.brand < 0) {
1254 const char letter = (value[0] >=
' ' && value[0] <=
'~') ?
static_cast<char>(value[0]) :
'?';
1255 ESP_LOGI(
detail::TAG,
"Device %s reports hot water brand '%c' (0x%02X)", device_id.c_str(), letter, value[0]);
1257 readings.brand = value[0];
1263bool store_dhw_reading(
const std::string &device_id,
DhwReadings &readings, uint8_t index,
const uint8_t *value,
1266 case DHW_REG_WATER_TEMPERATURE:
1268 case DHW_REG_MODE: {
1272 readings.mode =
static_cast<int16_t
>(mode);
1275 case DHW_REG_HEATING: {
1279 readings.
heating = state.heating ? 1 : 0;
1280 readings.element = state.element ? 1 : 0;
1281 readings.heat_pump = state.heat_pump ? 1 : 0;
1284 case DHW_REG_BOOST_DURATION:
1285 case DHW_REG_AWAY_DURATION:
1286 return store_dhw_duration(device_id, readings, index, value, len);
1287 case DHW_REG_ENERGY: {
1291 readings.energy_wh = wh;
1295 return store_dhw_brand(device_id, readings, value, len);
1302constexpr uint8_t DHW_POLL_REGISTERS[] = {DHW_REG_WATER_TEMPERATURE, DHW_REG_MODE, DHW_REG_HEATING,
1303 DHW_REG_BOOST_DURATION, DHW_REG_AWAY_DURATION, DHW_REG_ENERGY};
1309 const char *label = dhw_register_label(index);
1312 const uint8_t *value =
nullptr;
1321 if (!store_dhw_reading(device_id, dev->
dhw, index, value, len)) {
1324 ESP_LOGD(
detail::TAG,
"%s reply from device %s carries no value this codec takes: %s", label, device_id.c_str(),
1354 this->poll_policy_.get_auth_poll_failures(device_id));
1358 for (
size_t i = 0; ok && i <
sizeof(DHW_POLL_REGISTERS); i++)
1363 if (ok && dev !=
nullptr && dev->
dhw.
brand < 0)
1372 this->
schedule_heating_poll_(device_id, configured_ms != 0 ? configured_ms : HEATING_POLL_DEFAULT_INTERVAL_MS);
1377 uint16_t main_parameter = 0;
1379 ESP_LOGW(
detail::TAG,
"Hot water target %.1f C for device %s rejected: outside %.0f-%.0f C",
1380 static_cast<double>(celsius), device_id.c_str(),
static_cast<double>(DHW_TARGET_MIN_C),
1381 static_cast<double>(DHW_TARGET_MAX_C));
1395 device_id, {action,
false},
1396 [](
const IoDevice &d) {
return has_feature(d.features, device_feature::WATER_HEATER); },
"water heater", build);
1415 const uint8_t *payload,
size_t payload_len) {
1422 ESP_LOGW(
detail::TAG,
"%s for device %s: failed to build frame", operation, device_id.c_str());
1428 if (!this->
exchange_heating_(device_id, dhw_register_label(index), request, response, EXCHANGE_RETRY_COUNT))
1432 const uint8_t *ack_value =
nullptr;
1436 bytes_to_hex(ack_value, ack_len, ack_hex,
sizeof(ack_hex));
1437 ESP_LOGI(
detail::TAG,
"Device %s: %s write acknowledged with value [%s]", device_id.c_str(), operation, ack_hex);
1448 if (payload_len == 0) {
1449 ESP_LOGW(
detail::TAG,
"Hot water mode %u for device %s rejected: not a mode the heater reports",
1450 static_cast<unsigned>(mode), device_id.c_str());
1453 if (!this->
write_dhw_register_(device_id,
"hot water mode", DHW_REG_MODE, payload, payload_len))
1465struct DhwOperatingFeatureNames {
1466 const char *feature;
1467 const char *duration;
1471constexpr DhwOperatingFeatureNames DHW_OPERATING_FEATURE_NAMES[] = {
1472 {
"hot water boost",
"hot water boost duration"},
1473 {
"hot water away",
"hot water away duration"},
1476 "one row per DhwOperatingFeature");
1481 const DhwOperatingFeatureNames &names = DHW_OPERATING_FEATURE_NAMES[
static_cast<size_t>(which)];
1488 ESP_LOGW(
detail::TAG,
"%s for device %s refused: the heater does not report it as supported (FP15 0x%04X)",
1489 names.feature, device_id.c_str(),
static_cast<unsigned>(dev->
dhw.
capabilities));
1498 if (!this->
write_dhw_register_(device_id, names.duration, wire.duration_register, payload, payload_len)) {
1499 ESP_LOGW(
detail::TAG,
"Device %s: %s write not acknowledged, sending the mode anyway", device_id.c_str(),
1504 const uint16_t word = on ? wire.on_word : DHW_OPERATING_MODE_OFF;
1506 const bool accepted =
1507 this->
execute_dhw_(device_id, on ? names.feature :
"hot water boost and away off",
1509 return create_execute_heating_with_fp16(request, this->node_id_, d.node_id, d.low_power,
1510 MAIN_PARAMETER_KEEP, word);
1529 if (queue_guard_rejects(
this, device_id, QUEUE_GUARD_COVER)) {
1538 uint8_t pending_tilt = 0;
1539 for (
const auto &op : this->
op_queue_) {
1541 pending_tilt = op.position;
1545 if (this->op_queue_.enqueue_set_position(device_id, position)) {
1547 "Coalesced SET_POSITION (pos=%u) + pending SET_TILT (tilt=%u) → SET_POSITION_AND_TILT for "
1549 position, pending_tilt, device_id.c_str());
1574 if (queue_guard_rejects(
this, device_id, QUEUE_GUARD_TILT)) {
1581 uint8_t pending_pos = 0;
1582 for (
const auto &op : this->
op_queue_) {
1584 pending_pos = op.position;
1588 if (this->op_queue_.enqueue_set_tilt(device_id, tilt_percent)) {
1590 "Coalesced pending SET_POSITION (pos=%u) + SET_TILT (tilt=%u) → "
1591 "SET_POSITION_AND_TILT for device %s",
1592 pending_pos, tilt_percent, device_id.c_str());
1597 uint8_t tilt_percent) {
1598 if (queue_guard_rejects(
this, device_id, QUEUE_GUARD_POSITION_AND_TILT)) {
1607 if (queue_guard_rejects(
this, device_id, QUEUE_GUARD_STATUS))
1645 ESP_LOGW(
detail::TAG,
"Discover & Pair pressed while the previous attempt was running: ignored. Put the device "
1646 "back into pairing mode and press again.");
1663 result.
action =
"scan_paired_devices";
1664 result.
message =
"a radio exchange was already in progress; press the button again once it finishes";
1672 if (queue_guard_rejects(
this, device_id, QUEUE_GUARD_LIGHT))
1678 if (queue_guard_rejects(
this, device_id, QUEUE_GUARD_FAN))
1688 if (queue_guard_rejects(
this, device_id, QUEUE_GUARD_LOCK))
1694 if (queue_guard_rejects(
this, device_id, QUEUE_GUARD_SWITCH))
1724 if (!opt.has_value())
1728 switch (operation.
type) {
bool rollback_optimistic(const std::string &device_id, bool failed_stop=false)
Withdraw every prediction for a device after the command that produced them failed,...
IoDevice * get(const std::string &device_id)
Retrieve a registered device by ID.
void confirm_optimistic_stop(const std::string &device_id)
Mark a device's STOP as delivered: a later rollback must not restore the movement it replaced.
const DebugInfo & get_debug() const
Read-only access to the current debug snapshot.
The main IO-Homecontrol component.
std::string describe_last_commander(const IoDevice &dev) const
Render a device's "last commanded by" string, resolving this hub's own node ID.
virtual bool set_lock_state(const std::string &device_id, bool locked)
Semantic lock helper for lock entities.
virtual bool set_fan_position(const std::string &device_id, uint8_t position)
Send a heat level to an exterior heater (a fan entity) as an IO position.
void schedule_background_poll_backoff_(const std::string &device_id, bool auth_like, bool device_moving=false)
Apply backoff after a failed background status poll and log the result.
EventReading
The two subscription-answer sensor polls (ADR 0064).
@ SUN_LEVEL
EVENT_LOCAL_SENSOR, IoDevice::sun_level.
@ BATTERY_LEVEL
EVENT_BATTERY_LEVEL_CHANGE, IoDevice::battery_level.
void execute_oneway_position_(const std::string &controller_id, uint8_t position)
Send a queued 1W numeric position.
virtual void queue_set_fan_position(const std::string &device_id, uint8_t position)
Async form of set_fan_position() that keeps radio work serialized on the main loop.
bool poll_dhw_device_(const std::string &device_id)
Poll a water heater: the status read for its target, capabilities and boost and away state (read_dhw_...
virtual bool set_device_position_and_tilt(const std::string &device_id, uint8_t position, uint8_t tilt_percent)
Set both position and tilt of a tilt-capable cover in one atomic command.
virtual bool set_device_tilt(const std::string &device_id, uint8_t tilt_percent)
Send a tilt command to a tilt‑capable cover.
virtual bool set_water_heater_mode(const std::string &device_id, DhwMode mode)
Set a water heater's operating mode: a write of register 0x0006 (encode_dhw_mode_write()).
void execute_oneway_enroll_(const std::string &controller_id)
Send a queued 1W enrollment (add-controller).
virtual bool send_heating_command(const std::string &device_id, HeatingFunction fn, float value)
Run one heating function on the heater's private register protocol (CMD_WRITE_PRIVATE 0x20): a write ...
virtual bool set_switch_state(const std::string &device_id, bool on)
Semantic binary helper for switch entities.
bool run_execute_operation_(const std::string &device_id, const ExecuteRequestSpec &spec, const std::function< bool(const IoDevice &)> &accepts, const char *rejection_profile, const std::function< bool(IoFrame &, const IoDevice &)> &build)
Funnel for the four execute-family operations: runs try_execute_operation_() and, on any false return...
virtual void queue_request_device_sun_level(const std::string &device_id)
Queue an async sun level poll; executed in loop() as a background operation.
bool execute_dhw_(const std::string &device_id, const char *action, const std::function< bool(IoFrame &, const IoDevice &)> &build)
Send one water heater EXECUTE (target or operating mode) through try_execute_operation_(),...
void arm_execute_confirmation_poll_(const std::string &device_id, bool for_stop)
Arm the confirming poll that follows a command, because a CMD_EXECUTE reply is never trusted for posi...
virtual void queue_set_device_tilt(const std::string &device_id, uint8_t tilt_percent)
Queue an async tilt update; returns immediately, executed in loop().
void execute_oneway_(F &&send)
Shared bookkeeping for every 1W transmit: mark the radio busy for the duration of send,...
bool request_event_reading_(const std::string &device_id, EventReading kind)
Shared body of request_device_sun_level() and request_device_battery_level(): defer while the device ...
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.
bool set_dhw_operating_mode_(const std::string &device_id, DhwOperatingFeature which, bool on)
Switch a water heater's boost or away through the operating-mode word (ADR 0081).
bool handle_error_response_(const std::string &device_id, const IoFrame &request, const IoFrame &response, uint32_t retry_after_fail_ms)
Handle an explicit CMD_ERROR_RESP refusal from the device: record the result code,...
virtual bool set_heating_temperature(const std::string &device_id, uint16_t main_parameter, const HeatingFp &fp)
Command a heater that carries a temperature or an alias in its main parameter (the heating temperatur...
virtual IoDevice * get_device(const std::string &device_id)
Retrieve a device by ID; returns nullptr if not found.
virtual void queue_request_device_status(const std::string &device_id)
Queue an async status request; returns immediately, executed in loop().
bool read_dhw_reading_(const std::string &device_id, uint8_t index, uint8_t max_tries)
Read one family-0x00 register of a water heater and fold its value into IoDevice::dhw.
PeriodicPollPolicy battery_level_poll_policy_
Schedule of the opt-in battery level poll.
TuningConfig tuning_
Runtime tuning overrides.
void log_exchange_unconfirmed_debug_(const char *device_id) const
Log the last exchange debug snapshot for an accepted-but-unconfirmed exchange, at INFO.
void process_pending_operation_()
Pop next pending operation from the queue and execute it (set position, request status,...
StatusPollPolicy poll_policy_
bool read_private_register_(const std::string &device_id, uint8_t reg_hi, uint8_t reg_lo, const char *label, IoFrame &response, uint8_t max_tries)
Read one private register raw: build the read request, run exchange_heating_() and check that the rep...
const IoDevice * water_heater_for_write_(const std::string &device_id, const char *operation)
The water heater device_id names, or nullptr when it is unknown, the hub is not set up yet,...
virtual bool read_heating_register(const std::string &device_id, HeatingFunction fn, float *value_out, uint8_t max_tries)
Read one register of a heater and fold the decoded value into IoDevice::heating.
void execute_oneway_unenroll_(const std::string &controller_id)
Send a queued 1W un-enrollment (remove-controller).
decisions::AttemptRequest cover_poll_attempts_(const std::string &device_id, bool scheduler_managed, bool state_unknown=false)
Attempts for a cover's status read: its read kind (a settling read or a look at a busy device,...
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...
virtual bool request_device_battery_level(const std::string &device_id)
Subscribe the device to EVENT_BATTERY_LEVEL_CHANGE and publish the level its answer carries.
virtual void queue_request_device_name(const std::string &device_id)
Queue an async device-name request; returns immediately, executed in loop().
ExchangeOutcome send_and_receive_(const IoFrame &request, IoFrame &response, uint32_t freq, const decisions::AttemptRequest &attempts=EXCHANGE_RETRY_COUNT)
Main request/response exchange with retry and automatic authentication.
virtual void queue_request_device_limitation(const std::string &device_id)
Queue an async minimum-limitation read for the rain sensor poll; executed in loop() as a background o...
virtual void queue_set_lock_state(const std::string &device_id, bool locked)
Async form of set_lock_state() that keeps radio work serialized on the main loop.
uint32_t last_pairing_end_ms_
millis() when the last pairing attempt ended; 0 before the first one (see queue_discover_and_pair()).
virtual void queue_set_light_position(const std::string &device_id, uint8_t position)
Async form of set_light_position() that keeps radio work serialized on the main loop.
PeriodicPollPolicy & event_reading_policy_(EventReading kind)
The schedule of kind's poll.
void poll_programme_period_(const std::string &device_id, const HeatingProfile &profile, uint8_t max_tries)
Read the temperature in force of a heater's programme, when the programme runs.
bool try_execute_operation_(const std::string &device_id, const ExecuteRequestSpec &spec, const std::function< bool(const IoDevice &)> &accepts, const char *rejection_profile, const std::function< bool(IoFrame &, const IoDevice &)> &build)
Shared skeleton for the four execute-family operations (position, named command, tilt,...
void log_exchange_debug_(const char *device_id) const
Log the last exchange debug snapshot (delegates to exchange_engine_).
ExchangeOutcome exchange_past_device_event_(const IoFrame &request, IoFrame &response, const decisions::AttemptRequest &attempts)
send_and_receive_() on the command channel, except that a device event report (decisions::is_device_e...
virtual bool set_water_heater_boost(const std::string &device_id, bool on)
Start or end a water heater's boost: a duration write of register 0x0000, then an EXECUTE with the op...
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...
OneWayTransmitter oneway_transmitter_
Owns the 1W controller identities, their rolling-sequence counters and the transmit burst.
decisions::AttemptRequest heater_poll_attempts_(const std::string &device_id, bool scheduler_managed)
Attempts for a heater's status read: the count-based budget heaters keep (decisions::scheduled_poll_m...
virtual bool request_device_limitation(const std::string &device_id)
Read the device's minimum limitation and derive its rain sensor state from the reply.
virtual void queue_set_device_position_and_tilt(const std::string &device_id, uint8_t position, uint8_t tilt_percent)
Queue an async combined position+tilt update; returns immediately, executed in loop().
virtual bool queue_device_command(const std::string &device_id, CoverCommand cmd)
Queue an async named command (STOP, FAVORITE, VENT, PARTIAL, PEDESTRIAN, FORCE_OPEN); returns immedia...
virtual void queue_request_device_battery_level(const std::string &device_id)
Queue an async battery level poll; executed in loop() as a background operation.
virtual bool set_water_heater_target(const std::string &device_id, float celsius)
Set a water heater's target temperature.
virtual bool discover_and_pair()
Discover and pair a device that is in pairing mode.
void apply_event_reading_(const std::string &device_id, EventReading kind, const std::optional< uint16_t > &reading, bool unanswered)
Fold one poll outcome into the device record: a reading replaces the value and clears the miss count;...
bool poll_heating_device_(const std::string &device_id)
Poll a heater: read its profile's poll registers (stopping at the first failure), then the programme'...
virtual bool set_heating_level(const std::string &device_id, uint8_t level)
Set the heating level of a heater that is commanded with levels (the plain electrical heater).
ExchangeEngine exchange_engine_
Owns all authenticated exchange and LBT/hop logic.
RequestResult exchange_and_record_(const std::string &device_id, const IoFrame &request, IoFrame &response, const RequestOptions &options)
Run one authenticated request/response exchange on the standard command channel and record its outcom...
RequestResult
What exchange_and_record_() found.
@ REPLY
The device answered; the caller interprets the reply frame.
@ UNCONFIRMED
A CMD_EXECUTE the device accepted but never closed; counted as success, no reply frame.
@ ERROR_REPLY
The device refused with CMD_ERROR_RESP; handle_error_response_() has recorded it.
@ FAILED
No usable reply (silence, or an unconfirmed acceptance of a request that needs a payload).
void schedule_heating_poll_(const std::string &device_id, uint32_t delay_ms)
Arm the heater's regular-cadence poll timer.
PeriodicPollPolicy rain_poll_policy_
Schedule of the opt-in rain sensor poll.
virtual bool request_device_sun_level(const std::string &device_id)
Subscribe the device to EVENT_LOCAL_SENSOR and publish the sun value its answer carries.
virtual bool set_light_state(const std::string &device_id, bool on)
Semantic binary helper for light entities.
bool write_dhw_register_(const std::string &device_id, const char *operation, uint8_t index, const uint8_t *payload, size_t payload_len)
Write one family-0x00 register of a water heater, then read it back.
void apply_rain_poll_result_(const std::string &device_id, RainSensorState reading, const std::string &limit_text, bool unanswered)
Fold one rain poll outcome into the device's rain sensor state and tell subscribers.
esphome::home_io_control::ManagementActionResult ManagementActionResult
Result payload used by hub-level management actions such as rename.
bool read_dhw_register_(const std::string &device_id, uint8_t index, const char *label, IoFrame &response, uint8_t max_tries)
Read one family-0x00 register of a water heater raw (read_private_register_() gated on device_feature...
ManagementActions management_actions_
Owns rename, identify, force-open, scan_paired_devices, and other hub-level HA actions.
virtual void queue_set_light_state(const std::string &device_id, bool on)
Async form of set_light_state() that keeps radio work serialized on the main loop.
void notify_device_update_(const std::string &id)
Fire all registered device update callbacks for the given device ID.
virtual void queue_set_device_position(const std::string &device_id, uint8_t position)
Queue an async position update; returns immediately, executed in loop().
virtual void queue_set_switch_state(const std::string &device_id, bool on)
Async form of set_switch_state() that keeps radio work serialized on the main loop.
bool exchange_heating_(const std::string &device_id, const char *label, const IoFrame &request, IoFrame &response, uint8_t max_tries)
The exchange every heating frame shares: send on the command channel, record the outcome (exchange co...
virtual bool set_device_position(const std::string &device_id, uint8_t position)
Send a position command to a device.
uint8_t node_id_[NODE_ID_SIZE]
virtual bool set_water_heater_away(const std::string &device_id, bool on)
Start or end a water heater's away (holiday) mode: a duration write of register 0x0005 (255,...
bool read_dhw_status_(const std::string &device_id, uint8_t max_tries)
Read a water heater's target, capabilities and boost and away state: the status read DHW_STATUS_FPI1 ...
virtual bool request_device_name(const std::string &device_id)
Request the stored device name from a device.
void check_heater_model_(const std::string &device_id, uint8_t max_tries)
The one-off model check of a heater kind that has model rows (device_offers_models()).
virtual void set_device_temperature_range(const std::string &device_id, float min_c, float max_c)
Set the temperature encoding range of a heater that carries a temperature in its main parameter (YAML...
bool execute_request_and_update_(const std::string &device_id, const IoFrame &request, bool warn_on_no_response, uint32_t retry_after_fail_ms=0, const decisions::AttemptRequest &attempts=EXCHANGE_RETRY_COUNT)
Shared request/response helper for high-level operations: exchange_and_record_() plus update_device_s...
bool execute_device_command_(const std::string &device_id, CoverCommand cmd)
Execute a named device command (STOP, FAVORITE, VENT, PARTIAL, PEDESTRIAN, FORCE_OPEN) via the authen...
void trigger_scan_paired_devices()
Entry point for the "Scan Paired Devices" button: run the roll-call and publish its report to the log...
virtual void queue_discover_and_pair()
Queue a pairing operation; executed in loop() when radio idle.
virtual bool request_device_status(const std::string &device_id)
Request current status from a device.
PeriodicPollPolicy sun_poll_policy_
Schedule of the opt-in sun level poll.
virtual bool set_light_position(const std::string &device_id, uint8_t position)
Send an arbitrary IO position (0-100) to a light entity.
void execute_oneway_command_(const std::string &controller_id, CoverCommand cmd)
Send a queued 1W named command.
void publish_result(const ManagementActionResult &result)
Publish a management result as one or more structured log lines (one call per line of result....
void api_scan_paired_devices()
Native API callback: run a roll-call scan and publish the result as a HA event.
bool send_position(const std::string &controller_id, uint8_t position)
Send a numeric position as the identity's controller.
bool send_unenrollment(const std::string &controller_id)
Un-register this identity from every device of its class currently in association mode (CMD 0x39) alo...
bool send_enrollment(const std::string &controller_id)
Register this identity as a controller on every device currently in association mode (the receiver's ...
bool send_command(const std::string &controller_id, CoverCommand cmd)
Send a named command as the identity's controller.
void enqueue_set_lock_state(const std::string &device_id, bool locked)
void enqueue_set_position_and_tilt(const std::string &device_id, uint8_t position, uint8_t tilt_percent)
Enqueue SET_POSITION_AND_TILT directly (no coalescing needed).
std::optional< PendingOperation > pop()
void enqueue_device_command(const std::string &device_id, CoverCommand cmd)
bool enqueue_request_status(const std::string &device_id)
Enqueue REQUEST_STATUS, suppressing duplicates.
bool enqueue_request_name(const std::string &device_id)
Enqueue REQUEST_NAME, suppressing duplicates.
bool enqueue_request_sun_level(const std::string &device_id)
Enqueue REQUEST_SUN_LEVEL, suppressing duplicates.
bool enqueue_request_limitation(const std::string &device_id)
Enqueue REQUEST_LIMITATION, suppressing duplicates.
void enqueue_set_fan_position(const std::string &device_id, uint8_t position)
Enqueue SET_FAN_POSITION with an IO position (0-100; 0 = full heat, 100 = off).
void enqueue_set_switch_state(const std::string &device_id, bool on)
bool enqueue_discover_and_pair()
Enqueue DISCOVER_AND_PAIR with elevated priority.
bool enqueue_request_battery_level(const std::string &device_id)
Enqueue REQUEST_BATTERY_LEVEL, suppressing duplicates.
void enqueue_set_light_position(const std::string &device_id, uint8_t position)
Enqueue SET_LIGHT_STATE with an arbitrary IO position (0-100), for dimmable lights.
Per-device periodic poll schedule, miss retry and miss counter for one poll kind.
void defer(const std::string &device_id, uint32_t delay_ms, uint32_t now)
Pull the device's next poll forward to delay_ms after now. Never pushes it later.
void on_poll_succeeded(const std::string &device_id)
Clear the unanswered-poll count.
bool on_poll_failed(const std::string &device_id)
Count one failed poll (saturating), with no retry: for a poll the device answered with something that...
bool first_error_reply(const std::string &device_id)
True the first time it is called for a device that answered the poll's request with an error reply,...
bool retry_armed(const std::string &device_id) const
True while a retry armed by on_poll_unanswered() has not been popped yet.
bool on_poll_unanswered(const std::string &device_id, uint32_t now)
Handle a poll the device left unanswered.
void mark_stop_settle(const std::string &device_id)
Mark the device's next poll as the one reading where an accepted STOP left it, so it is a settling re...
void on_exchange_ended(uint32_t now, uint32_t delay_ms)
An exchange ended at now.
bool take_state_unknown(const std::string &device_id)
Consume the mark set by mark_state_unknown(): true once, for the status read dispatched after it.
bool is_tracking_active(const std::string &device_id, uint32_t now) const
True when the device has an active bounded polling window (deadline set and not expired).
uint32_t get_next_update(const std::string &device_id) const
uint32_t get_interval(const std::string &device_id) const
Return the configured interval (0 when not set or unconfigured).
void clear_failure_streaks(const std::string &device_id)
Reset failure streaks (and skipped moving misses) after a successful exchange response.
void set_next_update(const std::string &device_id, uint32_t abs_time)
Schedule the next poll at an absolute millis() timestamp.
uint8_t get_status_poll_failures(const std::string &device_id) const
void clear(const std::string &device_id)
Stop all polling for a device and reset failure counts. Preserves interval_ms.
bool is_predicted_end_read(const std::string &device_id) const
True when the next poll is the predicted-end read (the mark take_settling_read() consumes).
Pure transition helpers for hub-owned exchange and pairing frame decisions.
Helpers shared only by the hub's own implementation files (hub_*.cpp).
bool is_repeatable_execute(const IoFrame &request)
True for a CMD_EXECUTE that has the same effect sent twice as sent once.
RainSensorState rain_state_from_limitation_reply(const LimitationStatus &status)
Rain state from a decoded limitation reply: the one place that knows which reply fields decide.
bool is_repeat_safe_command(const IoFrame &request)
Whether a request may be sent again to a device that said nothing, without a second action.
std::string format_heater_model_name(const uint8_t *bytes, size_t len)
Render a heater's model-name register (0x0119, ASCII) for a log line.
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,...
ReadKind scheduled_poll_read_kind(uint8_t status_poll_failures, uint8_t auth_poll_failures, bool settles_a_position)
What a cover's status poll is for, from where it sits and what it settles.
bool is_subscription_answer(const IoFrame &request, const IoFrame &response)
Returns true when response is the answer to the subscription request: the request is a CMD_SUBSCRIBE_...
@ LOOKS_LIKE_TOWEL_DRYER
Declared as no towel dryer, but the towel-dryer mode register holds a value.
@ LOOKS_LIKE_RADIATOR
Declared a towel dryer, but the towel-dryer mode register holds no value.
@ NONE
The answer fits the declaration: nothing to say.
HeaterModelHint classify_heater_model_hint(DeviceModel declared, bool towel_mode_has_value)
Compare a heater's declared model with whether it answered the towel-dryer mode register (0x0136) wit...
@ SETTLING
A read that decides where a device ended up: as many attempts as fit the budget.
@ COMMAND
A request that is harmless to repeat (is_repeat_safe_command()): to a mains device as many attempts a...
uint8_t scheduled_poll_max_tries(uint8_t status_poll_failures, uint8_t auth_poll_failures)
Transmit-attempt budget for a scheduler-owned status poll of a heater, by ladder position.
constexpr uint8_t HEATER_MODEL_CHECK_MAX_RUNS
Runs of the one-off heater model check before the hub gives up on it for this boot.
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 describe_limitation_reply(const IoFrame &frame)
Render a CMD_LIMITATION_STATUS_RESP frame for a log line or probe report.
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).
bool known_device_supports_status_requests(const IoDevice &dev)
Does the device support status requests?
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.
std::string format_limitation_value(uint16_t value_raw)
Render a limitation value: a percentage, or the raw selector when it is not a position.
void update_link_health(IoDevice &dev, RadioDriver *radio)
Update per-device link-health stats from the radio's last capture.
bool is_settling(const IoDevice &dev)
True while a device that reported stopped off its target waits for the confirming poll.
void record_exchange_outcome(IoDevice &dev, uint8_t request_cmd, ExchangeOutcome outcome)
Record what an outbound exchange's ending says about the device, whichever way the caller then classi...
bool is_binary_entity_position(uint8_t position)
Is the given position value an on/off binary encoding?
std::string describe_last_commander(const IoDevice &dev, const uint8_t *hub_node_id)
Render the "Last Commanded By" sensor string.
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.
bool create_force_open(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t open_position)
Build a force-open execute frame (0x00): an ordinary position command to the device's wire-scale "ful...
void clear_inactive_programme_reading(const HeatingProfile &profile, HeatingReadings &readings)
Drop the temperature in force from the readings when the programme does not run.
bool create_get_name(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build a get-name request (0x50).
constexpr size_t HEATING_REPLY_VALUE_OFFSET
Offset of the first value byte in a register reply: the four header bytes it echoes.
void bytes_to_hex(const uint8_t *data, uint8_t len, char *out, size_t out_size)
bool decode_dhw_mode(const uint8_t *value, size_t len, DhwMode &mode)
Operating mode from the value of register 0x0006.
bool create_get_status(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build a get-status request (0x03). The device responds with its current position.
bool decode_limitation_status(const IoFrame &f, LimitationStatus &out)
Decode a limitation reply (assumed layout, see LimitationStatus).
bool decode_heating_reply(const IoFrame &reply, HeatingFunction fn, float &value)
Decode the value a heater answered a register read or write with.
constexpr size_t HEATING_PAYLOAD_MAX_SIZE
Largest payload any function produces: a set of a 16-bit value (4-byte header + 2 value bytes).
RainSensorState
What the hub's latest minimum-limitation read says about a rain-protection limit.
@ UNKNOWN
No usable reading: never read, or the last reads went unanswered.
bool create_write_private(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, const uint8_t *payload, size_t payload_len)
Build a generic CMD_WRITE_PRIVATE (0x20) frame around a caller-supplied payload — the one builder beh...
@ SET_LOCK_STATE
set_lock_state call (locked/unlocked).
@ SET_TILT
set_device_tilt call (tilt percentage 0–100).
@ SET_POSITION_AND_TILT
Combined set_device_position_and_tilt call.
@ SET_LIGHT_STATE
set_light_state call (binary on/off).
@ SET_FAN_POSITION
set_fan_position call (heat level of an exterior heater as an IO position).
@ REQUEST_BATTERY_LEVEL
request_device_battery_level call (subscribe to EVENT_BATTERY_LEVEL_CHANGE, read the answer).
@ SET_SWITCH_STATE
set_switch_state call (binary on/off).
@ REQUEST_NAME
request_device_name call (poll for stored device name).
@ ONEWAY_COMMAND
1W named command sent as a controller identity.
@ REQUEST_SUN_LEVEL
request_device_sun_level call (subscribe to EVENT_LOCAL_SENSOR, read the answer).
@ DISCOVER_AND_PAIR
discover_and_pair call (starts 3-phase pairing flow).
@ REQUEST_STATUS
request_device_status call (poll for current position).
@ ONEWAY_UNENROLL
1W remove-controller (0x39) un-registering an identity.
@ DEVICE_COMMAND
Named device command (STOP, FAVORITE, VENT).
@ SET_POSITION
set_device_position call (position 0–100 or special values).
@ REQUEST_LIMITATION
request_device_limitation call (read the device's minimum limitation).
@ ONEWAY_ENROLL
1W add-controller (0x30) registering an identity.
@ ONEWAY_POSITION
1W numeric position sent as a controller identity.
@ FP9
Functional parameter 9, one byte.
@ FP16
Functional parameter 16, two bytes.
@ NONE
A main-parameter-only command.
DhwMode
Operating mode as register 0x0006 holds it.
@ AUTO
The heater's automatic mode (its own schedule and target).
size_t encode_heating_payload(HeatingFunction fn, float value, uint8_t out[HEATING_PAYLOAD_MAX_SIZE])
Encode the function the heating_control action runs: a read for a read-only register,...
const HeatingProfile * find_heating_profile(DeviceType type, uint8_t subtype, DeviceModel model)
Look up the heating profile for a (type, subtype, model).
const char * rain_sensor_state_name(RainSensorState state)
Get a human-readable name for a RainSensorState.
HeatingFunction
Registers and functions of a 2W heater, one row of the descriptor table each.
@ TOWEL_MODE
Register 0x0136, read/write: a towel dryer's mode (TowelDryerMode).
constexpr uint8_t HEATING_REGISTER_HIGH_BYTE
Payload byte at index 2: the high byte of the 16-bit register number, constant 0x01.
bool heating_function_is_readable(HeatingFunction fn)
Whether the register behind a function can be read.
DeviceCapabilityClass device_capability_class(DeviceType type)
Map a raw IO‑Homecontrol type to the closest ESPHome/Home Assistant entity family.
bool create_limitation_status_read(IoFrame &f, const uint8_t *own, const uint8_t *dst, LimitationType type, bool low_power)
Build the limitation read (see the header for the evidence and the read-only guarantee).
CoverCommand
Named device commands for cover-type actuators.
@ STOP
Stop movement immediately.
@ FORCE_OPEN
Move to fully open at elevated priority; intended to bypass soft locks and environmental limits (conf...
bool decode_dhw_heating_state(const uint8_t *value, size_t len, DhwHeatingState &state)
What is heating, from the value of register 0x0021.
bool create_execute_heating_level(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t level)
Build a heating-level execute frame (0x00). See proto_commands.h.
@ TOWEL_DRYER
Towel dryer under the electrical heater with an adjustable setpoint (52/1).
bool decode_sun_level_event(const IoFrame &frame, uint16_t &out)
Decode the sun value of an EVENT_LOCAL_SENSOR report.
bool programme_period_active(const HeatingProfile &profile, const HeatingReadings &readings)
Does the heater's own programme run right now, with no timed run on top of it?
DhwOperatingFeature
The two features the operating-mode word switches; dhw_operating_feature_wire() holds their wire fact...
const DeviceTypeInfo * find_device_model_info(DeviceType type, uint8_t subtype, DeviceModel model)
Look up the model row with exactly this (type, subtype, model), without falling back.
const NamedPosition * find_named_position(CoverCommand command)
The named position a command sends.
bool create_execute_position_and_tilt(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t position, uint8_t tilt_percent)
Build a combined position-and-tilt execute command (0x00) — setClosureAndOrientation.
@ PRIVATE_REGISTERS
CMD_WRITE_PRIVATE register reads and writes (proto_heating.h).
const char * heating_function_name(HeatingFunction fn)
Stable lowercase name for a heating function ("power_on", "set_temperature", ...).
const char * device_operation_profile_name(DeviceType type, uint8_t subtype)
Human‑readable operation profile name for a device kind.
bool encode_dhw_target(float celsius, uint16_t &main_parameter)
Encode a target temperature as a main parameter over DHW_TARGET_MIN_C..DHW_TARGET_MAX_C.
constexpr bool has_feature(DeviceFeatures set, DeviceFeatures bit)
True when every bit of bit is set in set.
uint8_t frame_length(const IoFrame &f)
Get total frame length from ctrl0.
bool create_execute_tilt(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t tilt_percent)
Build a tilt execute command (0x00) for devices that support slat angle control.
bool create_execute_heating_main_parameter(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint16_t main_parameter)
Build a temperature / alias execute frame (0x00), main parameter only. See proto_commands....
bool create_execute_position(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t position, bool silent)
Build a position execute command (0x00) to move a device to a numeric position.
bool create_execute_heating_with_fp16(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint16_t main_parameter, uint16_t fp16)
Build a temperature / alias execute frame (0x00) carrying functional parameter 16....
const char * cover_command_name(CoverCommand cmd)
Get a human-readable name for a CoverCommand.
bool heating_reply_echoes_register(const IoFrame &reply, uint8_t reg_hi, uint8_t reg_lo)
Is this frame a register reply that echoes a request for this register?
bool create_execute_command(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, CoverCommand cmd, bool silent)
Build a named-command execute frame (0x00) for STOP or a named position (NAMED_POSITIONS).
bool decode_dhw_water_temperature(const uint8_t *value, size_t len, float &celsius)
Water temperature from the value of register 0x0017: signed 16-bit big-endian tenths.
ExchangeOutcome
Authenticated exchange engine — outbound and inbound protocol flows.
@ SUCCESS_WITH_RESPONSE
Device replied; the caller's response frame is populated.
@ SUCCESS_UNCONFIRMED
Device authenticated the request — so it received and accepted it — but sent no final response.
@ FAILED
No usable reply; the device may never have heard the request.
constexpr size_t FRAME_LOG_HEX_BUFFER_SIZE
Fits a full 32-byte frame rendered as spaced hex text.
bool device_offers_models(DeviceType type, uint8_t subtype)
Does this exact (type, subtype) have model rows below it?
bool device_uses_heating_levels(DeviceType type, uint8_t subtype)
Does a climate entity drive this device with heating levels?
bool decode_dhw_energy_wh(const uint8_t *value, size_t len, uint32_t &wh)
Energy since installation from the value of register 0x0004: unsigned 32-bit big-endian,...
bool effective_is_stopped(const IoDevice &dev)
Whether a consumer should treat the device as at rest, prediction first.
bool create_status_mp_fp_read(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t fpi1, uint8_t fpi2)
Build the KLF-style status read of the main parameter plus functional parameters (see the header for ...
bool create_subscribe_event(IoFrame &f, const uint8_t *own, const uint8_t *dst, uint16_t event_id, bool low_power)
Build the subscribe request (see the header for the allowlist).
constexpr uint8_t HEATING_REGISTER_MODEL_NAME
Register 0x0119: the heater's model name, ASCII, not terminated ("ADELIS 2 DIGIT" on an Atlantic Adel...
size_t encode_dhw_duration_write(DhwOperatingFeature which, uint8_t out[HEATING_PAYLOAD_MAX_SIZE])
Encode the duration write a start of which sends before its FP16 EXECUTE: 0C 61 00 00 01 for boost (D...
void note_moving_evidence(IoDevice &dev, uint32_t now_ms)
Record that a device is (believed to be) travelling right now.
bool decode_dhw_register_reply(const IoFrame &reply, uint8_t index, const uint8_t *&value, size_t &len)
The value bytes of a reply to a family-0x00 register read.
size_t encode_heating_register_read(uint8_t reg_hi, uint8_t reg_lo, uint8_t out[HEATING_PAYLOAD_MAX_SIZE])
Encode a read ("get") of an arbitrary register into a CMD_WRITE_PRIVATE (0x20) payload.
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.
bool device_uses_heating_registers(DeviceType type, uint8_t subtype)
Does a climate entity drive this device over the private register protocol?
size_t encode_dhw_mode_write(uint8_t mode, uint8_t out[HEATING_PAYLOAD_MAX_SIZE])
Encode a write of the operating mode: 0C 61 00 06 <mode>.
const DhwOperatingFeatureWire & dhw_operating_feature_wire(DhwOperatingFeature which)
The wire facts of which: the one place each feature's FP15 bit, FP16 field and duration live.
bool create_get_status_tilt(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build a tilt-aware get-status request (0x03) that returns the extended 16-byte tilt payload.
ProgrammePeriodState resolve_programme_period(const HeatingProfile &profile, const HeatingReadings &readings)
Which period of its programme a heater is in, and the temperature it heats to.
@ MINIMUM
Resulting minimum limit; rain protection raises it (a floor on the position value).
bool device_uses_heating_temperature(DeviceType type, uint8_t subtype)
Does a climate entity drive this device with a temperature or an alias in the main parameter?
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?
@ CLIMATE
Climate device (heating/cooling).
@ SWITCH
Binary on/off switch.
@ UNKNOWN
Unknown capability.
@ HEATER_LEVEL
Heater with a heat level in the position field (exterior heating).
@ COVER
Position‑controlled cover (shutter/blind/awning).
@ LIGHT
Binary on/off light.
bool decode_battery_level_event(const IoFrame &frame, uint8_t &out)
Decode the level of an EVENT_BATTERY_LEVEL_CHANGE report as a percentage.
bool device_accepts_register_probe(DeviceType type, uint8_t subtype)
May the diagnostic register-read probes (register_00, register_01) be sent to this device?
@ SILENT
Profile 0x05, the manufacturer app's silent operation: slower than the motor's own.
@ MOTOR_DEFAULT
The 6-byte frame with no profile: the motor's own speed, as its wall pad moves it.
bool create_execute_heating_with_mode_flags(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint16_t main_parameter, uint16_t mode_flags)
Build a temperature / alias execute frame (0x00) carrying functional parameter 9. See proto_commands....
bool heating_function_register(HeatingFunction fn, uint8_t ®ister_low)
Low byte of the 0x01xx register behind a function.
constexpr uint8_t HEATING_DIRECTION_GET
Payload byte at index 1 of a read ("get") request.
constexpr bool device_accepts_main_parameter(DeviceFeatures features, uint16_t main_parameter)
True when a device with these features accepts this main-parameter value in an EXECUTE.
bool create_execute_main_parameter(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t position, bool force_open_priority)
Build a main-parameter-only position execute frame (0x00). See proto_commands.h.
bool decode_dhw_duration(const uint8_t *value, size_t len, uint8_t &duration)
A duration from the value of register 0x0000 or 0x0005: one byte.
uint8_t serialize(const IoFrame &f, uint8_t *buf, uint8_t buf_size)
Serialize a parsed frame into a wire buffer (without CRC).
Command builders for the IO‑Homecontrol protocol.
The IO node-type and subtype table: names, entity class, platform and behaviour flags.
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.
One row of the node-type / subtype table.
const char * name
Type row: lowercase identifier ("roller_shutter").
What register 0x0021 says is heating.
bool heating
Byte 0: the heater is heating at all (heat pump, element or both).
The wire facts of one DhwOperatingFeature: what FP15, FP16 and the duration registers say about it.
What the hub has read from a domestic hot water heater.
int16_t brand
Brand letter (register 0x0007); -1 until read.
int32_t capabilities
FP15 operating-mode capabilities (DHW_CAPABILITY_*); -1 until read.
bool saw_challenge
True if a 0x3C was seen during this exchange.
uint8_t tries
Retry count (1-based).
A functional parameter and its value.
uint16_t value
Its value (one byte for FP9, two for FP16).
HeatingFpKind kind
Which parameter; NONE sends none.
std::span< const HeatingFunction > boot_registers
PRIVATE_REGISTERS: registers read once until they have been read.
std::span< const HeatingFunction > poll_registers
PRIVATE_REGISTERS: registers every poll reads, in order.
const HeatingProgrammePeriod * programme_period
PRIVATE_REGISTERS: how the heater's own programme names the comfort or eco period it is in; nullptr f...
HeatingControlPath path
How the heater is commanded and read.
std::span< const HeatingFunction > extra_poll_registers
PRIVATE_REGISTERS: registers every poll reads after poll_registers, for a part the heater may not hav...
How a heater's own weekly programme shows which period runs: the registers that tell,...
HeatingFunction in_force_fn
Register of the temperature in force.
HeatingFunction setback_fn
Register of the eco setback, a distance below the comfort setpoint.
bool has(HeatingFunction fn) const
Has this register been read yet?
bool clear(HeatingFunction fn)
Forget a register's value: it reads as "never read" again.
int16_t towel_mode
TowelDryerMode value; -1 until read.
Everything one execute-family operation needs beyond its own guard and frame builder.
const char * action
Verb/phrase for the "Sending ..." and rejection logs.
bool settle_as_stop
Passed through to arm_execute_confirmation_poll_().
Per-request knobs of exchange_and_record_().
uint32_t retry_after_fail_ms
If non-zero, drives the status-poll backoff and failure streaks.
decisions::AttemptRequest attempts
Attempt cap or read kind forwarded to send_and_receive_().
bool warn_on_no_response
Log a warning when no response arrives.
Runtime state of a paired IO‑Homecontrol device.
uint8_t model_check_attempts
Runs of the hub's one-off model check (a heater kind with model rows: model name read,...
float temperature_range_min_c
Lower end of the temperature encoding range of a heater that carries a temperature in its main parame...
DeviceFeatures features
What this device can do, resolved by resolve_device_features().
HeatingReadings heating
What the hub has read from a heater over the private register protocol (room temperature,...
std::optional< uint16_t > sun_level
Raw sun value from the opt-in EVENT_LOCAL_SENSOR poll.
bool low_power
YAML-declared: this target is a low-power / duty-cycled receiver, so directed frames set CTRL1_LOW_PO...
std::optional< uint16_t > battery_level
Battery level in percent (0-100) from the opt-in EVENT_BATTERY_LEVEL_CHANGE poll (decode_battery_leve...
uint8_t subtype
Device subtype (manufacturer‑specific).
DeviceModel model
YAML-declared product model below the subtype (io_model:); never on the wire.
float temperature_range_max_c
Upper end of that range (YAML max_temperature:).
uint8_t node_id[NODE_ID_SIZE]
Device's 3‑byte radio address.
DhwReadings dhw
What the hub has read from a water heater (device_feature::WATER_HEATER): family-0x00 registers and t...
RainSensorState rain_sensor
Rain state from the opt-in minimum-limitation poll.
bool model_checked
The model check completed: both of its reads were answered.
DeviceType type
Device type (shutter, awning, etc.).
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 data_len
Actual length of data.
One decoded CMD_LIMITATION_STATUS_RESP payload.
uint16_t value_raw
data[1..2], big-endian, position coding (STATUS_POS_MAX = 100 %).
std::string action
Action name, e.g. "rename_device".
std::string message
Human-readable outcome summary.
One named position: its feature bit, its main-parameter value and the command that sends it.
uint16_t main_parameter
Value sent as the EXECUTE main parameter.
const char * name
cover_command_name() of the command ("FAVORITE", "VENT", "PEDESTRIAN", "PARTIAL").
A single queued operation to be dispatched from loop().
CoverCommand command
Named command for DEVICE_COMMAND operations.
std::string device_id
Target device ID (hex string, e.g., "123ABC") — except for the ONEWAY_* types, where it carries the c...
uint8_t tilt
Tilt value for SET_POSITION_AND_TILT (0–100).
uint8_t position
Position/tilt value (0–100) or binary state (ON=0, OFF=100).
PendingOperationType type
Operation type (determines which handler to invoke).
int8_t preset_index
Index into the profile's presets of the period, or -1 when the temperature in force fits neither peri...
uint32_t remote_activity_catchup_poll_ms
Read every linked device this long after the last exchange of a burst of linked-remote activity; 0 = ...
What a caller asks of one exchange's attempts: a plain cap on tries (today's shape),...
bool is_progress_look() const
True for a look at a device that is probably still busy: one attempt, the ladder is the retry (ReadKi...
static AttemptRequest of_kind(ReadKind read_kind)
AttemptRequest with_wake_preamble_first() const
Mark the read as aimed at the moment the receiver has just come to rest: to a low-power target every ...