33constexpr size_t EXECUTE_ACTION_BUF_SIZE = 40;
37constexpr uint8_t FORCE_OPEN_WIRE_POSITION = 0;
38constexpr uint8_t FORCE_OPEN_WIRE_POSITION_INVERTED = 100;
44const char *position_command_action(
const IoDevice &dev, uint8_t position) {
45 if (position == POS_STOP)
49 return "set position";
51 bool const active_state = position == BINARY_ENTITY_ON_POSITION;
56 return active_state ?
"turn on" :
"turn off";
58 return active_state ?
"unlock" :
"lock";
65 return active_state ?
"open" :
"close";
74const char *operation_profile_name(
const IoDevice &dev) {
76 return "dimmable_light";
84const char *position_rejection_profile(
const IoDevice &dev, uint8_t position) {
105 bool (*accepts)(
const IoDevice &dev);
106 const char *rejection_noun;
107 const char *expected;
110constexpr QueueGuard QUEUE_GUARD_COVER{
112 "queued cover command",
"cover entity"};
114 "queued tilt command",
"tilt-capable cover"};
115constexpr QueueGuard QUEUE_GUARD_POSITION_AND_TILT{
117 "tilt-capable cover"};
118constexpr QueueGuard QUEUE_GUARD_LIGHT{
120 "queued light command",
"light entity"};
121constexpr QueueGuard QUEUE_GUARD_FAN{
123 "queued fan command",
"exterior heating (fan entity)"};
124constexpr QueueGuard QUEUE_GUARD_LOCK{
126 "queued lock command",
"lock entity"};
127constexpr QueueGuard QUEUE_GUARD_SWITCH{
129 "queued switch command",
"switch entity"};
130constexpr QueueGuard QUEUE_GUARD_STATUS{
132 "status-capable actuator"};
138bool queue_guard_rejects(
IOHomeControlComponent *hub,
const std::string &device_id,
const QueueGuard &guard) {
139 const IoDevice *dev = hub->get_device(device_id);
140 if (dev !=
nullptr && !guard.accepts(*dev)) {
153 if (existing != 0 && existing < millis() + delay_ms)
178 const bool unconfirmed_counts_as_success = request.
cmd == CMD_EXECUTE;
199 if (dbg.saw_challenge)
203 if (retry_after_fail_ms != 0)
206 if (warn_on_no_response) {
207 ESP_LOGW(
detail::TAG,
"Command 0x%02X failed for device %s: no valid response (stage=%s tries=%u)", request.
cmd,
208 device_id.c_str(), dbg.stage, dbg.tries);
227 if (retry_after_fail_ms != 0)
236 if (response.
cmd == CMD_ERROR_RESP) {
241 if (retry_after_fail_ms != 0)
249 bool warn_on_no_response, uint32_t retry_after_fail_ms,
253 this->
exchange_and_record_(device_id, request, response, {warn_on_no_response, retry_after_fail_ms, max_tries})) {
272 const IoFrame &response, uint32_t retry_after_fail_ms) {
281 }
else if (dev !=
nullptr) {
288 if (retry_after_fail_ms != 0)
294 const std::function<
bool(
const IoDevice &)> &accepts,
295 const char *rejection_profile,
309 }
else if (accepted) {
323 const std::function<
bool(
const IoDevice &)> &accepts,
324 const char *rejection_profile,
333 if (!accepts(*dev)) {
340 ESP_LOGI(
detail::TAG,
"Sending %s to device %s (profile=%s)", spec.
action, device_id.c_str(),
341 operation_profile_name(*dev));
344 if (!build(request, *dev)) {
357 const auto *dev = this->
get_device(device_id);
363 device_id, {position_command_action(*dev, position), position == POS_STOP},
365 position_rejection_profile(*dev, position),
383 ESP_LOGW(
detail::TAG,
"Device %s: %s is not supported on this device (%s)", device_id.c_str(),
389 request, this->
node_id_, d.node_id, d.low_power,
390 d.inverted ? FORCE_OPEN_WIRE_POSITION_INVERTED : FORCE_OPEN_WIRE_POSITION,
399 d.inverted ? FORCE_OPEN_WIRE_POSITION_INVERTED : FORCE_OPEN_WIRE_POSITION)
405 char action[EXECUTE_ACTION_BUF_SIZE];
406 snprintf(action,
sizeof(action),
"tilt=%u%%", tilt_percent);
409 "tilt-capable cover",
416 uint8_t tilt_percent) {
417 char action[EXECUTE_ACTION_BUF_SIZE];
418 snprintf(action,
sizeof(action),
"position=%u%% tilt=%u%%", position, tilt_percent);
421 "tilt-capable cover",
462 const uint32_t retry_after_fail_ms = scheduler_managed ? STATUS_RETRY_AFTER_FAIL_MS : 0;
463 const uint8_t max_tries =
465 this->poll_policy_.get_auth_poll_failures(device_id),
466 this->poll_policy_.take_stop_settle(device_id))
467 : EXCHANGE_RETRY_COUNT;
469 if (heating !=
nullptr) {
472 this->
schedule_heating_poll_(device_id, configured_ms != 0 ? configured_ms : HEATING_POLL_DEFAULT_INTERVAL_MS);
485 ESP_LOGD(
detail::TAG,
"Device %s: rain sensor poll deferred, device is moving", device_id.c_str());
503 std::string limit_text;
507 uint8_t raw[FRAME_MAX_SIZE] = {0};
508 const uint8_t raw_len =
serialize(response, raw,
sizeof(raw));
516 ESP_LOGD(
detail::TAG,
"Device %s: unexpected reply 0x%02X to the limitation read", device_id.c_str(),
520 ESP_LOGW(
detail::TAG,
"Device %s answers the limitation read with an error; its rain sensor stays unknown",
529 const std::string &limit_text) {
541 ESP_LOGI(
detail::TAG,
"Device %s: rain sensor %s -> %s%s%s%s", device_id.c_str(),
543 limit_text.empty() ?
"" :
" (minimum limit ", limit_text.c_str(), limit_text.empty() ?
"" :
")");
592 return this->
set_light_position(device_id, on ? BINARY_ENTITY_ON_POSITION : BINARY_ENTITY_OFF_POSITION);
606 return this->
set_device_position(device_id, on ? BINARY_ENTITY_ON_POSITION : BINARY_ENTITY_OFF_POSITION);
621 return this->
set_device_position(device_id, locked ? BINARY_ENTITY_OFF_POSITION : BINARY_ENTITY_ON_POSITION);
628 device_id, {
"heating level",
false},
638 device_id, {
"heating temperature",
false},
640 "temperature-controlled climate device",
645 main_parameter, fp.
value);
665 IoFrame &response, uint8_t max_tries) {
669 const char *direction = is_read ?
"read" :
"write";
670 ESP_LOGI(
detail::TAG,
"Sending heating %s %s to device %s", direction, label, device_id.c_str());
674 const bool acknowledged = got_reply && response.
cmd == CMD_WRITE_PRIVATE_ACK;
691 if (got_reply && response.
cmd == CMD_ERROR_RESP && response.
data_len > 0) {
693 }
else if (acknowledged) {
701 ESP_LOGW(
detail::TAG,
"Heating %s %s: no CMD_WRITE_PRIVATE_ACK from device %s", direction, label,
710 ESP_LOGD(
detail::TAG,
"heating %s %s ACK payload: %s", direction, label, ack_payload_hex);
727 if (payload_len == 0) {
728 ESP_LOGW(
detail::TAG,
"Heating command %s for device %s rejected: value %.2f out of range",
763 if (payload_len == 0) {
785 device_id.c_str(), payload_hex);
794 dev->heating.store(fn, value);
796 if (value_out !=
nullptr)
819 this->poll_policy_.get_auth_poll_failures(device_id));
845 this->
schedule_heating_poll_(device_id, configured_ms != 0 ? configured_ms : HEATING_POLL_DEFAULT_INTERVAL_MS);
850 if (queue_guard_rejects(
this, device_id, QUEUE_GUARD_COVER)) {
859 uint8_t pending_tilt = 0;
862 pending_tilt = op.position;
866 if (this->op_queue_.enqueue_set_position(device_id, position)) {
868 "Coalesced SET_POSITION (pos=%u) + pending SET_TILT (tilt=%u) → SET_POSITION_AND_TILT for "
870 position, pending_tilt, device_id.c_str());
895 if (queue_guard_rejects(
this, device_id, QUEUE_GUARD_TILT)) {
902 uint8_t pending_pos = 0;
905 pending_pos = op.position;
909 if (this->op_queue_.enqueue_set_tilt(device_id, tilt_percent)) {
911 "Coalesced pending SET_POSITION (pos=%u) + SET_TILT (tilt=%u) → "
912 "SET_POSITION_AND_TILT for device %s",
913 pending_pos, tilt_percent, device_id.c_str());
918 uint8_t tilt_percent) {
919 if (queue_guard_rejects(
this, device_id, QUEUE_GUARD_POSITION_AND_TILT)) {
928 if (queue_guard_rejects(
this, device_id, QUEUE_GUARD_STATUS))
954 ESP_LOGW(
detail::TAG,
"Discover & Pair pressed while the previous attempt was running: ignored. Put the device "
955 "back into pairing mode and press again.");
972 result.
action =
"scan_paired_devices";
973 result.
message =
"a radio exchange was already in progress; press the button again once it finishes";
981 if (queue_guard_rejects(
this, device_id, QUEUE_GUARD_LIGHT))
987 if (queue_guard_rejects(
this, device_id, QUEUE_GUARD_FAN))
997 if (queue_guard_rejects(
this, device_id, QUEUE_GUARD_LOCK))
1003 if (queue_guard_rejects(
this, device_id, QUEUE_GUARD_SWITCH))
1033 if (!opt.has_value())
1037 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.
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, uint8_t max_tries=EXCHANGE_RETRY_COUNT)
Shared request/response helper for high-level operations: exchange_and_record_() plus update_device_s...
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.
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.
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...
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,...
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 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().
void log_exchange_unconfirmed_debug_(const char *device_id) const
Log the last exchange debug snapshot for an accepted-but-unconfirmed exchange, at INFO.
ExchangeOutcome send_and_receive_(const IoFrame &request, IoFrame &response, uint32_t freq, uint8_t max_tries=EXCHANGE_RETRY_COUNT)
Main request/response exchange with retry and automatic authentication.
void process_pending_operation_()
Pop next pending operation from the queue and execute it (set position, request status,...
StatusPollPolicy poll_policy_
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).
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 void queue_request_device_name(const std::string &device_id)
Queue an async device-name request; returns immediately, executed in loop().
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.
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_).
void schedule_background_poll_backoff_(const std::string &device_id, bool auth_like)
Apply backoff after a failed background status poll and log the result.
OneWayTransmitter oneway_transmitter_
Owns the 1W controller identities, their rolling-sequence counters and the transmit burst.
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, FORCE_OPEN); returns immediately,...
void apply_rain_poll_result_(const std::string &device_id, RainSensorState reading, const std::string &limit_text)
Fold one rain poll outcome into the device's rain sensor state and tell subscribers.
virtual bool discover_and_pair()
Discover and pair a device that is in pairing mode.
bool poll_heating_device_(const std::string &device_id)
Poll a heater: read its room temperature, setpoint, mode and regulator state (stopping at the first f...
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.
virtual bool set_light_state(const std::string &device_id, bool on)
Semantic binary helper for light entities.
esphome::home_io_control::ManagementActionResult ManagementActionResult
Result payload used by hub-level management actions such as rename.
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 request_device_name(const std::string &device_id)
Request the stored device name from a device.
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_device_command_(const std::string &device_id, CoverCommand cmd)
Execute a named device command (STOP, FAVORITE, VENT, FORCE_OPEN) via the authenticated exchange.
RainPollPolicy rain_poll_policy_
Schedule of the opt-in rain sensor poll.
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.
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_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.
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.
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.
bool first_error_reply(const std::string &device_id)
True the first time it is called for a device that answered the limitation read with an error reply,...
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 unanswered poll (saturating).
void mark_stop_settle(const std::string &device_id)
Mark the device's next poll as the one confirming an accepted STOP, so it gets STOP_SETTLE_POLL_TRIES...
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 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.
Pure transition helpers for hub-owned exchange and pairing frame decisions.
Helpers shared only by the hub's own implementation files (hub_*.cpp).
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.
uint8_t scheduled_poll_max_tries(uint8_t status_poll_failures, uint8_t auth_poll_failures, bool settles_a_stop=false)
Transmit-attempt budget for a scheduler-owned status poll, by backoff-ladder position.
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.
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.
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 log_frame_issue(IOHomeControlComponent *component, const char *direction, const char *reason, const IoFrame &frame, uint8_t len)
Log a frame‑level issue (unregistered endpoints, unsupported commands).
void 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...
const char * device_operation_profile_name(DeviceType type)
Human‑readable operation profile name for a device type.
bool create_get_name(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build a get-name request (0x50).
uint32_t settle_delay_ms(uint32_t interval_ms, uint32_t hint_delay_ms, bool cap_for_stop)
Resolve the follow-up settle-poll delay while a device may still be moving.
void bytes_to_hex(const uint8_t *data, uint8_t len, char *out, size_t out_size)
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 device_takes_main_parameter_only(DeviceType type, uint8_t subtype)
Does this device take movement commands as the main parameter only?
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).
@ 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.
@ 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.
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 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.
const HeatingProfile * find_heating_profile(DeviceType type, uint8_t subtype)
Look up the heating profile for a (type, subtype).
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.
@ FAVORITE
Move to stored favorite/"My" position.
@ STOP
Stop movement immediately.
@ VENT
Move to ventilation position (window-type devices).
@ FORCE_OPEN
Move to fully open at elevated priority; intended to bypass soft locks and environmental limits (conf...
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.
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", ...).
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 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, FAVORITE, or VENT.
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_uses_heating_levels(DeviceType type, uint8_t subtype)
Does a climate entity drive this device with heating levels?
bool effective_is_stopped(const IoDevice &dev)
Whether a consumer should treat the device as at rest, prediction first.
void note_moving_evidence(IoDevice &dev, uint32_t now_ms)
Record that a device is (believed to be) travelling right now.
bool device_uses_heating_registers(DeviceType type, uint8_t subtype)
Does a climate entity drive this device over the private register protocol?
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.
@ 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?
@ 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 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....
size_t encode_heating_read(HeatingFunction fn, uint8_t out[HEATING_PAYLOAD_MAX_SIZE])
Encode a read ("get") of one register into a CMD_WRITE_PRIVATE (0x20) payload.
constexpr uint8_t HEATING_DIRECTION_GET
Payload byte at index 1 of a read ("get") request.
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.
std::string format_device_kind(DeviceType type, uint8_t subtype)
Human-readable description of a (type, subtype) for logs and reports.
bool device_supports_tilt(DeviceType type)
Does this device type support tilt (slat angle) control?
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.
Heating profiles: what a climate card offers for each kind of heater, and how.
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.
const HeatingFunction * boot_registers
PRIVATE_REGISTERS: registers read once until they have been read.
size_t boot_register_count
Number of entries in boot_registers.
const HeatingFunction * poll_registers
PRIVATE_REGISTERS: registers every poll reads, in order.
size_t poll_register_count
Number of entries in poll_registers.
HeatingControlPath path
How the heater is commanded and read.
bool has(HeatingFunction fn) const
Has this register been read yet?
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_().
uint8_t max_tries
Transmit-attempt cap forwarded to send_and_receive_().
uint32_t retry_after_fail_ms
If non-zero, drives the status-poll backoff and failure streaks.
bool warn_on_no_response
Log a warning when no response arrives.
Runtime state of a paired IO‑Homecontrol device.
float temperature_range_min_c
Lower end of the temperature encoding range of a heater that carries a temperature in its main parame...
HeatingReadings heating
What the hub has read from a heater over the private register protocol (room temperature,...
bool low_power
YAML-declared: this target is a low-power / duty-cycled receiver, so directed frames set CTRL1_LOW_PO...
uint8_t subtype
Device subtype (manufacturer‑specific).
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.
RainSensorState rain_sensor
Rain state from the opt-in minimum-limitation poll.
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.
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).