|
Home IO Control
ESPHome add-on for IO-Homecontrol devices
|
Encapsulates hub-level management operations exposed as Home Assistant actions. More...
#include <management_actions.h>
Public Member Functions | |
| ManagementActions (const uint8_t *node_id, const uint8_t *system_key, const TuningConfig *tuning, ExchangeEngine &engine, DeviceRegistry ®istry, const bool *initialized, IOHomeControlComponent *hub) | |
| Construct with all required collaborators. | |
| ManagementActions (const ManagementActions &)=delete | |
| Non-copyable — stores references and pointers into hub member addresses. | |
| ManagementActions & | operator= (const ManagementActions &)=delete |
| void | register_actions () |
| Register all management actions (rename, identify, force-open, ...) with ESPHome's native API server. | |
| void | api_rename_device (const std::string &device_id, const std::string &new_name) |
| Native API callback: rename a device and publish the result as a HA event. | |
| ManagementActionResult | rename_device (const std::string &device_id, const std::string &new_name) |
| Rename a registered device and verify the result by reading the name back. | |
| void | api_identify_device (const std::string &device_id) |
| Native API callback: trigger a device's physical identify and publish the result as a HA event. | |
| ManagementActionResult | identify_device (const std::string &device_id) |
| Trigger a registered device's physical identify (brief jog/flash). | |
| void | api_force_open_device (const std::string &device_id) |
| Native API callback: force-open a device and publish the result as a HA event. | |
| ManagementActionResult | force_open_device (const std::string &device_id) |
| Move a registered cover device to fully open at elevated priority, intended to bypass wind/rain soft locks. | |
| void | api_scan_paired_devices () |
| Native API callback: run a roll-call scan and publish the result as a HA event. | |
| void | api_oneway_set_position (const std::string &controller_id, const std::string &position) |
| Native API callback: queue a 1W position for a controller identity. | |
| void | api_oneway_remove_controller (const std::string &controller_id) |
| Native API callback: queue a standalone 1W un-enrollment (remove-controller, CMD 0x39) for a controller identity. | |
| ManagementActionResult | scan_paired_devices () |
| Broadcast a roll-call and report every device that answers. | |
| void | api_probe_device (const std::string &device_id, const std::string &probe, const std::string &index) |
| Native API callback: run a single diagnostic probe and publish the result as a HA event. | |
| ManagementActionResult | probe_device (const std::string &device_id, const std::string &probe, const std::string &index) |
| Send a single diagnostic probe frame to an already-paired device and report the raw reply. | |
| void | api_probe_sweep (const std::string &device_id, const std::string &probe, const std::string &first_index, const std::string &last_index) |
| Native API callback: run a bounded probe sweep and publish the result as a HA event. | |
| ManagementActionResult | probe_sweep (const std::string &device_id, const std::string &probe, const std::string &first_index, const std::string &last_index) |
| Walk a bounded index range, one probe_device() call per index, in one user gesture. | |
| void | api_heating_control (const std::string &device_id, const std::string &function, const std::string &value) |
| Native API callback: run a heating/climate function and publish the result as a HA event. | |
| ManagementActionResult | heating_control (const std::string &device_id, const std::string &function, const std::string &value) |
| Send one 2W heating/climate function (CMD_WRITE_PRIVATE 0x20) to a registered climate device. | |
| void | publish_result (const ManagementActionResult &result) |
| Publish a management result as one or more structured log lines (one call per line of result.message, see log_multiline_result() in the .cpp) and a Home Assistant event. | |
Encapsulates hub-level management operations exposed as Home Assistant actions.
ManagementActions is constructed once by IOHomeControlComponent. It stores pointers and references into hub members; it is non-copyable for the same reason as ExchangeEngine.
Definition at line 67 of file management_actions.h.
| esphome::home_io_control::ManagementActions::ManagementActions | ( | const uint8_t * | node_id, |
| const uint8_t * | system_key, | ||
| const TuningConfig * | tuning, | ||
| ExchangeEngine & | engine, | ||
| DeviceRegistry & | registry, | ||
| const bool * | initialized, | ||
| IOHomeControlComponent * | hub ) |
Construct with all required collaborators.
| node_id | Controller 3-byte node ID, owned by the hub. |
| system_key | Controller 16-byte AES system key, owned by the hub. Only scan_paired_devices() uses this — it builds a self-authenticating CMD_DISCOVER_SPE_REQ itself, unlike rename/identify/force-open, which authenticate through ExchangeEngine's challenge-response. |
| tuning | Runtime tuning config, owned by the hub. Only scan_paired_devices() uses this — it reuses pairing_discovery_wait_ms as the listen window for each of its own per-channel attempts. |
| engine | Shared exchange engine for radio transactions. |
| registry | Device registry for device lookups. |
| initialized | Pointer to the hub's initialized flag. |
| hub | Hub pointer for ESPHome API calls and public hub methods. |
Definition at line 516 of file management_actions.cpp.
|
delete |
Non-copyable — stores references and pointers into hub member addresses.
| void esphome::home_io_control::ManagementActions::api_force_open_device | ( | const std::string & | device_id | ) |
Native API callback: force-open a device and publish the result as a HA event.
Definition at line 802 of file management_actions.cpp.
| void esphome::home_io_control::ManagementActions::api_heating_control | ( | const std::string & | device_id, |
| const std::string & | function, | ||
| const std::string & | value ) |
Native API callback: run a heating/climate function and publish the result as a HA event.
Definition at line 826 of file management_actions.cpp.
| void esphome::home_io_control::ManagementActions::api_identify_device | ( | const std::string & | device_id | ) |
Native API callback: trigger a device's physical identify and publish the result as a HA event.
Definition at line 760 of file management_actions.cpp.
| void esphome::home_io_control::ManagementActions::api_oneway_remove_controller | ( | const std::string & | controller_id | ) |
Native API callback: queue a standalone 1W un-enrollment (remove-controller, CMD 0x39) for a controller identity.
The direct caller of 0x39 in this codebase (ADR 0006 — a rare rollback path gets an action, not a permanent entity) — the "Enroll" button also fires this same frame, but only as the documented prelude immediately before its own 0x30, never on its own. Same "queued, not confirmed" framing as api_oneway_set_position() — 1W has no reply, so nothing here can ever say a device actually forgot this identity.
| controller_id | Controller-identity handle from oneway_controllers:. |
Definition at line 916 of file management_actions.cpp.
| void esphome::home_io_control::ManagementActions::api_oneway_set_position | ( | const std::string & | controller_id, |
| const std::string & | position ) |
Native API callback: queue a 1W position for a controller identity.
An action rather than an entity per percentage (ADR 0006). position arrives as a string because every argument on this action surface is a string; it is parsed and range-checked here, and a bad value is reported rather than coerced — a malformed position that silently became 0 would send a fully-open command to a whole device class.
The published result says only that the command was queued. 1W has no reply, so nothing downstream can ever upgrade that to "the device moved"; the per-identity "Last 1W Command" sensor reports what was transmitted.
| controller_id | Controller-identity handle from oneway_controllers:. |
| position | Target position 0-100 as a decimal string. |
Definition at line 881 of file management_actions.cpp.
| void esphome::home_io_control::ManagementActions::api_probe_device | ( | const std::string & | device_id, |
| const std::string & | probe, | ||
| const std::string & | index ) |
Native API callback: run a single diagnostic probe and publish the result as a HA event.
Definition at line 1100 of file management_actions.cpp.
| void esphome::home_io_control::ManagementActions::api_probe_sweep | ( | const std::string & | device_id, |
| const std::string & | probe, | ||
| const std::string & | first_index, | ||
| const std::string & | last_index ) |
Native API callback: run a bounded probe sweep and publish the result as a HA event.
Definition at line 1199 of file management_actions.cpp.
| void esphome::home_io_control::ManagementActions::api_rename_device | ( | const std::string & | device_id, |
| const std::string & | new_name ) |
Native API callback: rename a device and publish the result as a HA event.
Definition at line 649 of file management_actions.cpp.
| void esphome::home_io_control::ManagementActions::api_scan_paired_devices | ( | ) |
Native API callback: run a roll-call scan and publish the result as a HA event.
Definition at line 879 of file management_actions.cpp.
| ManagementActionResult esphome::home_io_control::ManagementActions::force_open_device | ( | const std::string & | device_id | ) |
Move a registered cover device to fully open at elevated priority, intended to bypass wind/rain soft locks.
Safety-sensitive: this deliberately requires an explicit device_id (a hub-level action, not a permanent per-device button) because it can override legitimate environmental protection. Queues the command onto the hub's normal cover-command dispatch path (IOHomeControlComponent::queue_device_command()), which already owns capability gating, poll tracking, and backoff — this method does not talk to the radio directly. The result therefore only confirms that the command was queued; the actual movement outcome arrives later via the device's normal cover-state/polling pipeline, so verified is always false.
| device_id | Target device ID (hex string, case-insensitive). |
Definition at line 806 of file management_actions.cpp.
| ManagementActionResult esphome::home_io_control::ManagementActions::heating_control | ( | const std::string & | device_id, |
| const std::string & | function, | ||
| const std::string & | value ) |
Send one 2W heating/climate function (CMD_WRITE_PRIVATE 0x20) to a registered climate device.
A real user feature, but an unvalidated one — the protocol is derived from the iohomecontrol project's Cozytouch support and has never been exercised against real Atlantic/Thermor/Sauter hardware (see docs/home_io_control.md's experimental banner). It is NOT diagnostic-gated; the guard is documentation, not diagnostic_probes:.
function is one of power_on / set_temperature / set_mode / set_presence / set_window / midnight_sync. value is parsed per function: a float in degrees Celsius (7.0-28.0) for set_temperature, auto|manual|prog|off for set_mode, on|off for set_presence, open|close for set_window, ignored for the other two. A malformed value is reported, never coerced.
Delegates to IOHomeControlComponent::send_heating_command() so the entity and the action share exactly one transmit path (see that method). verified is always false: the set_* functions are write-only — nothing decodes what the radiator did into an entity. (power_on and midnight_sync are register reads; their ACK payload is logged at DEBUG but not decoded.)
| device_id | Target device ID (hex string, case-insensitive). |
| function | Heating function name (see above). |
| value | Function-specific value string (see above). |
Definition at line 831 of file management_actions.cpp.
| ManagementActionResult esphome::home_io_control::ManagementActions::identify_device | ( | const std::string & | device_id | ) |
Trigger a registered device's physical identify (brief jog/flash).
Deliberately performs no device-type gating beyond "is it registered on this hub" — identify exists precisely to let a user work out what an unknown registry entry physically is, so restricting it to specific device types would defeat the purpose. A CMD_ERROR_RESP reply is treated as a successful trigger (not a failure): per the protocol, devices may answer an identify request that way and still perform the jog.
| device_id | Target device ID (hex string, case-insensitive). |
Definition at line 764 of file management_actions.cpp.
|
delete |
| ManagementActionResult esphome::home_io_control::ManagementActions::probe_device | ( | const std::string & | device_id, |
| const std::string & | probe, | ||
| const std::string & | index ) |
Send a single diagnostic probe frame to an already-paired device and report the raw reply.
Protocol-research instrumentation for opcodes this codebase has not decoded — see docs/radio_diagnostics.md and ADR 0024. Refuses unless this build was configured with diagnostic_probes: true (IOHomeControlComponent::diagnostic_probes_enabled()), and separately refuses while the target device is moving (!effective_is_stopped(*dev)) — an unknown frame into a mid-transaction device state machine is the one avoidable way a read-shaped probe could cause harm. That is the device's last reported movement state or a standing hub prediction that it is moving (a command the hub just issued, not yet confirmed): both warrant the same refusal. It is always "stopped" (never refuses) for a light/switch, and the reported state can be stale if the device was last moved from a physical remote the hub never saw. Routes the response straight into result.message as raw hex; it is never passed to the hub's status decoder (update_device_status_() is not reachable from this class at all — see hub_core.h), so a probe can never be misread as a position update. verified is always false: the whole point is that the reply's meaning is not yet known.
probe selects the frame builder:
index is a string, not an int, because ManagementServiceDescriptor:: encode_list_service_response() hardcodes every native-API action argument as SERVICE_ARG_TYPE_STRING (see the .cpp) — parsed here via parse_probe_index(), which accepts both "6" and "0x06".
| device_id | Target device ID (hex string, case-insensitive). |
| probe | One of the probe names above. |
| index | Function ID / selector block / modifier, depending on probe; a decimal or 0x-prefixed hex string. Ignored for "general_info3". |
Definition at line 1105 of file management_actions.cpp.
| ManagementActionResult esphome::home_io_control::ManagementActions::probe_sweep | ( | const std::string & | device_id, |
| const std::string & | probe, | ||
| const std::string & | first_index, | ||
| const std::string & | last_index ) |
Walk a bounded index range, one probe_device() call per index, in one user gesture.
Answers Q0/Q1/Q3 (the multi-value probes) without one native-API call per value. Bounded to PROBE_SWEEP_MAX_INDICES indices and spaced PROBE_SWEEP_DELAY_MS apart — this transmits on a shared ISM band to what may be a battery device, so a tight loop is antisocial and would also keep the device awake and skew results. Each index costs up to EXCHANGE_RETRY_COUNT frames (proto_timing.h), same as a single probe_device() call.
| device_id | Target device ID (hex string, case-insensitive). |
| probe | One of probe_device()'s probe names. |
| first_index | First index in the sweep (inclusive), same string format as probe_device(). |
| last_index | Last index in the sweep (inclusive). |
Definition at line 1204 of file management_actions.cpp.
| void esphome::home_io_control::ManagementActions::publish_result | ( | const ManagementActionResult & | result | ) |
Publish a management result as one or more structured log lines (one call per line of result.message, see log_multiline_result() in the .cpp) and a Home Assistant event.
Definition at line 653 of file management_actions.cpp.
| void esphome::home_io_control::ManagementActions::register_actions | ( | ) |
Register all management actions (rename, identify, force-open, ...) with ESPHome's native API server.
Definition at line 527 of file management_actions.cpp.
| ManagementActionResult esphome::home_io_control::ManagementActions::rename_device | ( | const std::string & | device_id, |
| const std::string & | new_name ) |
Rename a registered device and verify the result by reading the name back.
| device_id | Target device ID (hex string, case-insensitive). |
| new_name | Requested UTF-8 device name. |
Definition at line 698 of file management_actions.cpp.
| ManagementActionResult esphome::home_io_control::ManagementActions::scan_paired_devices | ( | ) |
Broadcast a roll-call and report every device that answers.
Not a discovery mechanism for new devices: only devices that already hold this hub's system key answer a CMD_DISCOVER_SPE_REQ (the 0x2A payload is self-authenticating — 6 random bytes plus a 6-byte HMAC over the command byte, computed with system_key_), so a device that has never paired with this hub stays silent. See tests/corpus/captures/discovery/somfy_awning_discovery_spe_paired_rollcall.yaml for a captured exchange. Never writes DeviceRegistry — every responder is only looked up, never registered; an unknown responder usually means a device paired earlier whose YAML config was never saved, not an intruder. result.device_id stays empty (there is no single target) and verified stays false (nothing here is read back). success is true whenever the broadcast went out, including with zero replies — "nothing answered" is a valid result, not a failure. result.message carries the full multi-line report: a header line (devices detected, how many known vs. unknown), then a Known: section followed by an Unknown: section (either omitted if empty) — known responders get a summary line each, unknown responders additionally get a lead-in sentence and a ready-to-paste YAML block. Devices are grouped by known/unknown rather than left in arrival order, since the two groups need different follow-up and interleaving them made an unknown responder easy to miss between known ones. At most SCAN_MAX_REPLIES distinct responders are reported; if more answer, the report says so explicitly rather than quietly listing a subset.
Transmits the request once per channel (CH2, then CH1, then CH3), each with its own full pairing_discovery_wait_ms listen window, merging distinct responders across attempts — a paired device only hears the broadcast if it happens to be awake on the channel the hub transmits on at that instant, and real hardware testing found that single-channel duty-cycling paired devices are not reliably caught by a one-shot broadcast. A responder that answers more than one attempt still appears exactly once. Each attempt is a single transmit followed by its own window, never back-to-back transmits, so it does not reintroduce the different failure mode a 3-channel-burst transmit caused elsewhere: firing three long-preamble transmits back-to-back with no listening in between blew through the tight per-try response wait windows and broke exchanges in both directions (see KeyExtractionResponder::broadcast_reply_()'s doc comment).
This still blocks the caller for the full three-window duration (roughly 3 × pairing_discovery_wait_ms, ~6 s at the 2000 ms default) and therefore trips ESPHome's "operation took a long time" warning on every invocation. That warning uses a per-component ratchet (Component::should_warn_of_blocking()): it starts at 50 ms and, each time it fires, raises its own threshold to the observed duration plus a margin — but the threshold is stored in centiseconds in a uint8_t, so it saturates at 2550 ms. Anything that blocks longer than that can never ratchet out of warning range.
Accepting that is a deliberate tradeoff (confirmed on real hardware 2026-08-10). A shorter fixed window (500 ms/attempt, ~2.4 s total) was tried: it would have gone quiet after one warning, since 2.4 s sits under the 2550 ms cap — but it also caused real, correctly-decoded replies from registered devices to arrive after the window had already closed, where the passive path drops them (hub_status.cpp's unhandled_cmd catch-all). Losing devices from the report is worse than a recurring log line, so the long window won. Getting both would require restructuring this action to run across multiple scheduled loop() ticks so no single blocking unit approaches 2550 ms — not done here.
Definition at line 1017 of file management_actions.cpp.