Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
management_actions.h
Go to the documentation of this file.
1#pragma once
2
3/// @file management_actions.h
4/// @brief Hub-level management operations exposed as Home Assistant actions.
5/// @ingroup hioc_hub
6///
7/// ManagementActions owns the family of advanced, non-entity hub actions (device rename,
8/// identify, force-open, ...) and the native API registration that wires them into Home
9/// Assistant. Every action is registered through one data-driven API service descriptor
10/// (detail::ManagementServiceDescriptor); adding a new action does not require a new
11/// descriptor class. The descriptor holds a ManagementActions* and calls only public
12/// methods on it, so no friend declarations into the hub are needed.
13///
14/// IOHomeControlComponent exposes this collaborator through thin protected wrappers
15/// (api_rename_device_, api_identify_device_, api_force_open_device_, api_scan_paired_devices_,
16/// register_management_actions_).
17
18#include "exchange_engine.h"
19#include "device_registry.h"
20
21#include <cstdint>
22#include <string>
23
24namespace esphome {
25namespace home_io_control {
26
27// Forward declarations — full definitions included only in management_actions.cpp.
29struct TuningConfig;
30
31/// Result of a hub-level management action such as rename.
32///
33/// Returned by ManagementActions::rename_device() and re-exported from the hub
34/// as IOHomeControlComponent::ManagementActionResult via a public type alias.
36 bool success{false}; ///< Whether the requested management action succeeded.
37 bool verified{false}; ///< Whether a follow-up readback confirmed the applied state.
38 bool has_result_code{false}; ///< True when result_code contains a decoded CMD_ERROR_RESP byte.
39 uint8_t result_code{0}; ///< Optional CMD_ERROR_RESP result byte.
40 std::string action; ///< Action name, e.g. "rename_device".
41 std::string device_id; ///< Target IO-homecontrol device ID.
42 std::string message; ///< Human-readable outcome summary.
43 std::string requested_name; ///< Requested normalized UTF-8 name for rename actions.
44 std::string applied_name; ///< Verified cached UTF-8 name after a readback, when available.
45};
46
47/// Encapsulates hub-level management operations exposed as Home Assistant actions.
48///
49/// ManagementActions is constructed once by IOHomeControlComponent. It stores pointers
50/// and references into hub members; it is non-copyable for the same reason as ExchangeEngine.
51/// @ingroup hioc_hub
53 public:
54 /// Construct with all required collaborators.
55 ///
56 /// @param node_id Controller 3-byte node ID, owned by the hub.
57 /// @param system_key Controller 16-byte AES system key, owned by the hub. Only
58 /// scan_paired_devices() uses this — it builds a self-authenticating
59 /// CMD_DISCOVER_SPE_REQ itself, unlike rename/identify/force-open, which
60 /// authenticate through ExchangeEngine's challenge-response.
61 /// @param tuning Runtime tuning config, owned by the hub. Only scan_paired_devices()
62 /// uses this — it reuses `pairing_discovery_wait_ms` as the listen window
63 /// for each of its own per-channel attempts.
64 /// @param engine Shared exchange engine for radio transactions.
65 /// @param registry Device registry for device lookups.
66 /// @param initialized Pointer to the hub's initialized flag.
67 /// @param hub Hub pointer for ESPHome API calls and public hub methods.
68 ManagementActions(const uint8_t *node_id, const uint8_t *system_key, const TuningConfig *tuning,
69 ExchangeEngine &engine, DeviceRegistry &registry, const bool *initialized,
71
72 /// Non-copyable — stores references and pointers into hub member addresses.
75
76 /// Register all management actions (rename, identify, force-open, ...) with ESPHome's
77 /// native API server.
78 void register_actions();
79
80 /// Native API callback: rename a device and publish the result as a HA event.
81 void api_rename_device(const std::string &device_id, const std::string &new_name);
82
83 /// Rename a registered device and verify the result by reading the name back.
84 /// @param device_id Target device ID (hex string, case-insensitive).
85 /// @param new_name Requested UTF-8 device name.
86 /// @return Structured result describing success, verification, and any error.
87 ManagementActionResult rename_device(const std::string &device_id, const std::string &new_name);
88
89 /// Native API callback: trigger a device's physical identify and publish the result as a HA event.
90 void api_identify_device(const std::string &device_id);
91
92 /// @brief Trigger a registered device's physical identify (brief jog/flash).
93 ///
94 /// Deliberately performs no device-type gating beyond "is it registered on this hub" — identify
95 /// exists precisely to let a user work out what an unknown registry entry physically is, so
96 /// restricting it to specific device types would defeat the purpose. A CMD_ERROR_RESP reply is
97 /// treated as a successful trigger (not a failure): per the protocol, devices may answer an
98 /// identify request that way and still perform the jog.
99 /// @param device_id Target device ID (hex string, case-insensitive).
100 /// @return Structured result describing success and any validation failure. `verified` is
101 /// always false — there is no readback that confirms a physical jog happened.
102 ManagementActionResult identify_device(const std::string &device_id);
103
104 /// Native API callback: force-open a device and publish the result as a HA event.
105 void api_force_open_device(const std::string &device_id);
106
107 /// @brief Move a registered cover device to fully open at elevated priority, intended to
108 /// bypass wind/rain soft locks.
109 ///
110 /// Safety-sensitive: this deliberately requires an explicit `device_id` (a hub-level action,
111 /// not a permanent per-device button) because it can override legitimate environmental
112 /// protection. Queues the command onto the hub's normal cover-command dispatch path
113 /// (IOHomeControlComponent::queue_device_command()), which already owns capability gating,
114 /// poll tracking, and backoff — this method does not talk to the radio directly. The result
115 /// therefore only confirms that the command was queued; the actual movement outcome arrives
116 /// later via the device's normal cover-state/polling pipeline, so `verified` is always false.
117 /// @note The elevated-priority mechanism (see create_force_open() in proto_commands.cpp) is
118 /// confirmed on real hardware to correctly move a device to fully open, but bypassing
119 /// an *active* environmental lock is still experimental — only that the device accepts
120 /// the frame and opens when nothing is locking it has been observed so far.
121 /// @param device_id Target device ID (hex string, case-insensitive).
122 /// @return Structured result describing whether the command was queued.
123 ManagementActionResult force_open_device(const std::string &device_id);
124
125 /// Native API callback: run a roll-call scan and publish the result as a HA event.
127
128 /// @brief Broadcast a roll-call and report every device that answers.
129 ///
130 /// Not a discovery mechanism for new devices: only devices that already hold this hub's
131 /// system key answer a CMD_DISCOVER_SPE_REQ (the 0x2A payload is self-authenticating — 6
132 /// random bytes plus a 6-byte HMAC over the command byte, computed with `system_key_`), so a
133 /// device that has never paired with this hub stays silent. See
134 /// `tests/corpus/captures/somfy_awning/discover_spe_paired_rollcall.yaml` for a captured
135 /// exchange. Never writes DeviceRegistry — every responder is only looked up, never
136 /// registered; an unknown responder usually means a device paired earlier whose YAML config
137 /// was never saved, not an intruder. `result.device_id` stays empty (there is no single
138 /// target) and `verified` stays false (nothing here is read back). `success` is true whenever
139 /// the broadcast went out, including with zero replies — "nothing answered" is a valid
140 /// result, not a failure. `result.message` carries the full multi-line report: a header line
141 /// (devices detected, how many known vs. unknown), then a `Known:` section followed by an
142 /// `Unknown:` section (either omitted if empty) — known responders get a summary line each,
143 /// unknown responders additionally get a lead-in sentence and a ready-to-paste YAML block.
144 /// Devices are grouped by known/unknown rather than left in arrival order, since the two
145 /// groups need different follow-up and interleaving them made an unknown responder easy to
146 /// miss between known ones. At most `SCAN_MAX_REPLIES` distinct responders are reported; if
147 /// more answer, the report says so explicitly rather than quietly listing a subset.
148 ///
149 /// Transmits the request once per channel (CH2, then CH1, then CH3), each with its own full
150 /// `pairing_discovery_wait_ms` listen window, merging distinct responders across attempts — a
151 /// paired device only hears the broadcast if it happens to be awake on the channel the hub
152 /// transmits on at that instant, and real hardware testing found that single-channel
153 /// duty-cycling paired devices are not reliably caught by a one-shot broadcast. A responder
154 /// that answers more than one attempt still appears exactly once. Each attempt is a single
155 /// transmit followed by its own window, never back-to-back transmits, so it does not
156 /// reintroduce the different failure mode a 3-channel-burst transmit caused elsewhere: firing
157 /// three long-preamble transmits back-to-back with no listening in between blew through the
158 /// tight per-try response wait windows and broke exchanges in both directions (see
159 /// `IOHomeControlComponent::broadcast_key_extraction_reply_()`'s doc comment).
160 ///
161 /// This still blocks the caller for the full three-window duration (roughly
162 /// `3 × pairing_discovery_wait_ms`, ~6 s at the 2000 ms default) and therefore trips ESPHome's
163 /// "operation took a long time" warning on *every* invocation. That warning uses a per-component
164 /// ratchet (`Component::should_warn_of_blocking()`): it starts at 50 ms and, each time it fires,
165 /// raises its own threshold to the observed duration plus a margin — but the threshold is stored
166 /// in centiseconds in a `uint8_t`, so it saturates at **2550 ms**. Anything that blocks longer
167 /// than that can never ratchet out of warning range.
168 ///
169 /// Accepting that is a deliberate tradeoff (confirmed on real hardware 2026-08-10). A shorter
170 /// fixed window (500 ms/attempt, ~2.4 s total) was tried: it would have gone quiet after one
171 /// warning, since 2.4 s sits under the 2550 ms cap — but it also caused real, correctly-decoded
172 /// replies from registered devices to arrive after the window had already closed, where the
173 /// passive path drops them (`hub_status.cpp`'s `unhandled_cmd` catch-all). Losing devices from
174 /// the report is worse than a recurring log line, so the long window won. Getting both would
175 /// require restructuring this action to run across multiple scheduled `loop()` ticks so no single
176 /// blocking unit approaches 2550 ms — not done here.
177 /// @return Structured result whose `message` is the full report.
179
180 /// Publish a management result as one or more structured log lines (one call per line of
181 /// `result.message`, see log_multiline_result() in the .cpp) and a Home Assistant event.
182 void publish_result(const ManagementActionResult &result);
183
184 private:
185 /// @brief Shared preamble for every management action: validate the hub and device ID,
186 /// then resolve the target device.
187 ///
188 /// Initializes `result` with the action name and normalized device ID, then checks (in
189 /// order) that the hub is initialized, that `device_id` is a well-formed 6-hex-digit ID,
190 /// and that it names a device registered on this hub. On any failure, sets `result.message`
191 /// to a stable, caller-facing string and returns nullptr; callers should return `result`
192 /// immediately in that case.
193 /// @param action Action name to record on `result` (e.g. MANAGEMENT_ACTION_RENAME_DEVICE).
194 /// @param device_id Requested device ID (hex string, case-insensitive, as received from HA).
195 /// @param result Populated with the action name and normalized device ID, plus an error
196 /// message on failure.
197 /// @return Pointer to the resolved device, or nullptr if any validation step failed.
198 IoDevice *resolve_device_(const char *action, const std::string &device_id, ManagementActionResult &result);
199
200 /// @brief Send an authenticated request and report a stock timeout failure on no response.
201 ///
202 /// Shared by every action that talks to the device directly (rename, identify): both send
203 /// their built request via ExchangeEngine::send_and_receive() and, on failure, log the same
204 /// exchange debug snapshot and report the same "no valid response to <action_verb> request"
205 /// message shape. Actions that don't talk to the radio directly (force-open, which queues
206 /// through the hub's cover-command dispatch path instead) have no use for this helper.
207 /// @param request Frame to send.
208 /// @param response Populated with the device's reply on success.
209 /// @param action_verb Bare verb used only in the failure message, e.g. "rename" or "identify".
210 /// @param result Result to update with `message` on failure; `result.device_id` must already be
211 /// set (via resolve_device_()) since it is used for the debug log.
212 /// @return true if a response was received; false on timeout (result.message set, debug
213 /// snapshot logged).
214 bool send_authenticated_request_(const IoFrame &request, IoFrame &response, const char *action_verb,
215 ManagementActionResult &result);
216
217 const uint8_t *node_id_;
218 const uint8_t *system_key_;
219 const TuningConfig *tuning_;
220 ExchangeEngine &engine_;
221 DeviceRegistry &registry_;
222 const bool *initialized_;
224};
225
226} // namespace home_io_control
227} // namespace esphome
Owns the per-hub device table, update callbacks, and linked-remote associations.
Authenticated exchange engine — outbound and inbound protocol flows.
The main IO-Homecontrol component.
Definition hub_core.h:74
ManagementActions(const ManagementActions &)=delete
Non-copyable — stores references and pointers into hub member addresses.
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 scan_paired_devices()
Broadcast a roll-call and report every device that answers.
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 publish_result(const ManagementActionResult &result)
Publish a management result as one or more structured log lines (one call per line of result....
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.
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.
ManagementActionResult identify_device(const std::string &device_id)
Trigger a registered device's physical identify (brief jog/flash).
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 ...
void api_scan_paired_devices()
Native API callback: run a roll-call scan and publish the result as a HA event.
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.
ManagementActions & operator=(const ManagementActions &)=delete
Per-hub device table, update-callback fan-out, and linked-remote map.
Self-contained authenticated exchange engine for IO-Homecontrol 2W.
Runtime state of a paired IO‑Homecontrol device.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:71
Result of a hub-level management action such as rename.
bool verified
Whether a follow-up readback confirmed the applied state.
std::string device_id
Target IO-homecontrol device ID.
uint8_t result_code
Optional CMD_ERROR_RESP result byte.
std::string action
Action name, e.g. "rename_device".
std::string applied_name
Verified cached UTF-8 name after a readback, when available.
bool success
Whether the requested management action succeeded.
bool has_result_code
True when result_code contains a decoded CMD_ERROR_RESP byte.
std::string message
Human-readable outcome summary.
std::string requested_name
Requested normalized UTF-8 name for rename actions.
All runtime tunable parameters for pairing and radio diagnostics.