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 std::string probe_name; ///< Probe name for probe_device()/probe_sweep(), e.g. "private_fn".
46 std::string probe_index; ///< Requested index argument, as received (before parsing).
47 bool has_response_cmd{false}; ///< True when response_cmd contains a probe reply's command byte.
48 uint8_t response_cmd{0}; ///< Probe reply's command byte (distinct from a CMD_ERROR_RESP
49 ///< result_code above, which is a different byte with a
50 ///< different meaning).
51 std::string response_hex; ///< Probe reply's full raw wire hex, for pasting into
52 ///< scripts/corpus/ingest.py.
53 bool terminal_refusal{false}; ///< True when probe_device() failed for a reason that will
54 ///< recur identically for every remaining index in a sweep
55 ///< (diagnostic probes not enabled, device moving) — set only by
56 ///< probe_device(), read only by probe_sweep() to decide
57 ///< whether to stop early. A structured flag rather than
58 ///< probe_sweep() pattern-matching `message` text, which would
59 ///< silently break if that text were ever reworded.
60};
61
62/// Encapsulates hub-level management operations exposed as Home Assistant actions.
63///
64/// ManagementActions is constructed once by IOHomeControlComponent. It stores pointers
65/// and references into hub members; it is non-copyable for the same reason as ExchangeEngine.
66/// @ingroup hioc_hub
68 public:
69 /// Construct with all required collaborators.
70 ///
71 /// @param node_id Controller 3-byte node ID, owned by the hub.
72 /// @param system_key Controller 16-byte AES system key, owned by the hub. Only
73 /// scan_paired_devices() uses this — it builds a self-authenticating
74 /// CMD_DISCOVER_SPE_REQ itself, unlike rename/identify/force-open, which
75 /// authenticate through ExchangeEngine's challenge-response.
76 /// @param tuning Runtime tuning config, owned by the hub. Only scan_paired_devices()
77 /// uses this — it reuses `pairing_discovery_wait_ms` as the listen window
78 /// for each of its own per-channel attempts, and reads `scan_power_classes`
79 /// to decide which passes to run.
80 /// @param engine Shared exchange engine for radio transactions.
81 /// @param registry Device registry for device lookups.
82 /// @param initialized Pointer to the hub's initialized flag.
83 /// @param hub Hub pointer for ESPHome API calls and public hub methods.
84 ManagementActions(const uint8_t *node_id, const uint8_t *system_key, const TuningConfig *tuning,
85 ExchangeEngine &engine, DeviceRegistry &registry, const bool *initialized,
87
88 /// Non-copyable — stores references and pointers into hub member addresses.
91
92 /// Register all management actions (rename, identify, force-open, ...) with ESPHome's
93 /// native API server.
94 void register_actions();
95
96 /// Native API callback: rename a device and publish the result as a HA event.
97 void api_rename_device(const std::string &device_id, const std::string &new_name);
98
99 /// Rename a registered device and verify the result by reading the name back.
100 /// @param device_id Target device ID (hex string, case-insensitive).
101 /// @param new_name Requested UTF-8 device name.
102 /// @return Structured result describing success, verification, and any error.
103 ManagementActionResult rename_device(const std::string &device_id, const std::string &new_name);
104
105 /// Native API callback: trigger a device's physical identify and publish the result as a HA event.
106 void api_identify_device(const std::string &device_id);
107
108 /// @brief Trigger a registered device's physical identify (brief jog/flash).
109 ///
110 /// Deliberately performs no device-type gating beyond "is it registered on this hub" — identify
111 /// exists precisely to let a user work out what an unknown registry entry physically is, so
112 /// restricting it to specific device types would defeat the purpose. A CMD_ERROR_RESP reply is
113 /// treated as a successful trigger (not a failure): per the protocol, devices may answer an
114 /// identify request that way and still perform the jog.
115 /// @param device_id Target device ID (hex string, case-insensitive).
116 /// @return Structured result describing success and any validation failure. `verified` is
117 /// always false — there is no readback that confirms a physical jog happened.
118 ManagementActionResult identify_device(const std::string &device_id);
119
120 /// Native API callback: force-open a device and publish the result as a HA event.
121 void api_force_open_device(const std::string &device_id);
122
123 /// @brief Move a registered cover device to fully open at elevated priority, intended to
124 /// bypass wind/rain soft locks.
125 ///
126 /// Safety-sensitive: this deliberately requires an explicit `device_id` (a hub-level action,
127 /// not a permanent per-device button) because it can override legitimate environmental
128 /// protection. Queues the command onto the hub's normal cover-command dispatch path
129 /// (IOHomeControlComponent::queue_device_command()), which already owns capability gating,
130 /// poll tracking, and backoff — this method does not talk to the radio directly. The result
131 /// therefore only confirms that the command was queued; the actual movement outcome arrives
132 /// later via the device's normal cover-state/polling pipeline, so `verified` is always false.
133 /// @note The elevated-priority mechanism (see create_force_open() in proto_commands.cpp) is
134 /// confirmed on real hardware to correctly move a device to fully open, but bypassing
135 /// an *active* environmental lock is still experimental — only that the device accepts
136 /// the frame and opens when nothing is locking it has been observed so far.
137 /// @param device_id Target device ID (hex string, case-insensitive).
138 /// @return Structured result describing whether the command was queued.
139 ManagementActionResult force_open_device(const std::string &device_id);
140
141 /// Native API callback: run a roll-call scan and publish the result as a HA event.
143
144 /// @brief Native API callback: queue a 1W position for a controller identity.
145 ///
146 /// An action rather than an entity per percentage (ADR 0006). `position` arrives as a string
147 /// because every argument on this action surface is a string; it is parsed and range-checked
148 /// here, and a bad value is reported rather than coerced — a malformed position that silently
149 /// became 0 would send a fully-open command to a whole device class.
150 ///
151 /// The published result says only that the command was *queued*. 1W has no reply, so nothing
152 /// downstream can ever upgrade that to "the device moved"; the per-identity "Last 1W Command"
153 /// sensor reports what was transmitted.
154 /// @param controller_id Controller-identity handle from `oneway_controllers:`.
155 /// @param position Target position 0-100 as a decimal string.
156 void api_oneway_set_position(const std::string &controller_id, const std::string &position);
157
158 /// @brief Native API callback: queue a standalone 1W un-enrollment (remove-controller, CMD
159 /// 0x39) for a controller identity.
160 ///
161 /// The direct caller of `0x39` in this codebase (ADR 0006 — a rare rollback path gets an
162 /// action, not a permanent entity) — the "Enroll" button also fires this same frame, but only
163 /// as the documented prelude immediately before its own `0x30`, never on its own. Same "queued,
164 /// not confirmed" framing as api_oneway_set_position() — 1W has no reply, so nothing here can
165 /// ever say a device actually forgot this identity.
166 ///
167 /// @warning **Unconfirmed standalone on real hardware.** An enrolled device has been observed
168 /// to ignore this frame sent alone — the hub kept controlling it afterwards. The frame itself
169 /// matches every real captured `0x39` this project holds (payload shape, MAC span), so the most
170 /// likely explanation is the same physical gate enrollment has: a device may only act on `0x39`
171 /// while its receiver is in the 2 s PROG association-mode window, and the failed attempt was
172 /// fired without it — untested, not ruled out. Do not treat this action as a confirmed rollback
173 /// until it is retested with that gesture.
174 /// @param controller_id Controller-identity handle from `oneway_controllers:`.
175 void api_oneway_remove_controller(const std::string &controller_id);
176
177 /// @brief Broadcast a roll-call and report every device that answers.
178 ///
179 /// Not a discovery mechanism for new devices: only devices that already hold this hub's
180 /// system key answer a CMD_DISCOVER_SPE_REQ (the 0x2A payload is self-authenticating — 6
181 /// random bytes plus a 6-byte HMAC over the command byte, computed with `system_key_`), so a
182 /// device that has never paired with this hub stays silent. See
183 /// `tests/corpus/captures/discovery/somfy_awning_discovery_spe_paired_rollcall.yaml` for a captured
184 /// exchange. Never writes DeviceRegistry — every responder is only looked up, never
185 /// registered; an unknown responder usually means a device paired earlier whose YAML config
186 /// was never saved, not an intruder. `result.device_id` stays empty (there is no single
187 /// target) and `verified` stays false (nothing here is read back). `success` is true whenever
188 /// the broadcast went out, including with zero replies — "nothing answered" is a valid
189 /// result, not a failure. `result.message` carries the full multi-line report: a header line
190 /// (devices detected, how many known vs. unknown), then a `Known:` section followed by an
191 /// `Unknown:` section (either omitted if empty) — known responders get a summary line each,
192 /// unknown responders additionally get a lead-in sentence and a ready-to-paste YAML block.
193 /// Devices are grouped by known/unknown rather than left in arrival order, since the two
194 /// groups need different follow-up and interleaving them made an unknown responder easy to
195 /// miss between known ones. At most `SCAN_MAX_REPLIES` distinct responders are reported; if
196 /// more answer, the report says so explicitly rather than quietly listing a subset.
197 ///
198 /// Sweeps two passes, low-power first, then always-alive — six attempts total (three channels
199 /// per pass): CH2/CH1/CH3 with `CTRL1 = LOW_POWER | ACK` and `LONG_PREAMBLE` (the header a real
200 /// VELUX KLR300 uses for its own roll-call, whose low-power shutters answered it), then
201 /// CH2/CH1/CH3 with `CTRL1 = 0x00` and the tunable `normal_start_preamble`. Low-power first so a sleeper that answers
202 /// late is still collected by the later always-alive windows; in the opposite order a late
203 /// low-power reply would fall off the end of the scan. The low-power pass listens on all three
204 /// channels including the one it transmitted on (real VELUX low-power replies have been
205 /// observed landing there); the always-alive pass listens only on the two it did not transmit
206 /// on (a Somfy always-alive reply lands on the request channel about 1 in 149 times). The
207 /// `scan_power_classes` tuning select (default `both`) narrows this to one pass, for bisecting
208 /// an installation in the field or restoring the shorter single-pass scan on an install with no
209 /// solar/battery devices. Each attempt gets its own full `pairing_discovery_wait_ms` listen
210 /// window, merging distinct responders across all six attempts — a paired device only hears the
211 /// broadcast if it happens to be awake on the channel the hub transmits on at that instant, and
212 /// real hardware testing found that single-channel duty-cycling paired devices are not reliably
213 /// caught by a one-shot broadcast. A responder that answers more than one attempt still appears
214 /// exactly once. Each attempt is a single transmit followed by its own window, never
215 /// back-to-back transmits, so it does not reintroduce the different failure mode a
216 /// multi-channel-burst transmit caused elsewhere: firing several long-preamble transmits
217 /// back-to-back with no listening in between blew through the tight per-try response wait
218 /// windows and broke exchanges in both directions (see
219 /// `KeyExtractionResponder::broadcast_reply_()`'s doc comment).
220 ///
221 /// This still blocks the caller for the full six-window duration (roughly
222 /// `6 × pairing_discovery_wait_ms` plus three ~213 ms LONG_PREAMBLE transmits, ~12.7 s at the
223 /// 2000 ms default) and therefore trips ESPHome's "operation took a long time" warning on
224 /// *every* invocation. That warning uses a per-component ratchet
225 /// (`Component::should_warn_of_blocking()`): it starts at 50 ms and, each time it fires, raises
226 /// its own threshold to the observed duration plus a margin — but the threshold is stored in
227 /// centiseconds in a `uint8_t`, so it saturates at **2550 ms**. Anything that blocks longer than
228 /// that can never ratchet out of warning range; doubling the scan adds no new warning class,
229 /// since the single-pass scan was already permanently past the cap.
230 ///
231 /// Accepting that is a deliberate tradeoff (confirmed on real hardware 2026-08-10). A shorter
232 /// fixed window — 500 ms per attempt, ~2.4 s for a three-attempt sweep (a single-pass total
233 /// under the 2550 ms cap) — was tried: it would have gone quiet after one warning, but it also
234 /// caused real, correctly-decoded replies from registered devices to arrive after the window
235 /// had already closed, where the passive path drops them (`hub_status.cpp`'s `unhandled_cmd`
236 /// catch-all). Losing devices from the report is worse than a recurring log line, so the long
237 /// window won. Getting both would require restructuring this action to run across multiple
238 /// scheduled `loop()` ticks so no single blocking unit approaches 2550 ms — not done here.
239 /// @return Structured result whose `message` is the full report.
241
242 /// Native API callback: run a single diagnostic probe and publish the result as a HA event.
243 void api_probe_device(const std::string &device_id, const std::string &probe, const std::string &index);
244
245 /// @brief Send a single diagnostic probe frame to an already-paired device and report the raw
246 /// reply.
247 ///
248 /// Protocol-research instrumentation for opcodes this codebase has not decoded — see
249 /// docs/diagnostic-probes.md and ADR 0024. Refuses unless
250 /// this build was configured with `diagnostic_probes: true`
251 /// (IOHomeControlComponent::diagnostic_probes_enabled()), and separately refuses while the
252 /// target device is moving (`!effective_is_stopped(*dev)`) — an unknown frame into a
253 /// mid-transaction device state machine is the one avoidable way a read-shaped probe could
254 /// cause harm. That is the device's last *reported* movement state *or* a standing hub
255 /// prediction that it is moving (a command the hub just issued, not yet confirmed): both
256 /// warrant the same refusal. It is always "stopped" (never refuses) for a light/switch, and the
257 /// reported state can be stale if the device was last moved from a physical remote the hub
258 /// never saw. Routes
259 /// the response straight into `result.message` as raw hex; it
260 /// is never passed to the hub's status decoder (`update_device_status_()` is not reachable
261 /// from this class at all — see hub_core.h), so a probe can never be misread as a position
262 /// update. `verified` is always false: the whole point is that the reply's meaning is not yet
263 /// known.
264 ///
265 /// `probe` selects the frame builder:
266 /// - "private_fn": create_private_function(), `index` = function ID (Q0).
267 /// - "private_fn_sub": create_private_function() at function ID 0x09, `index` = the second
268 /// payload byte (data[1]) — an undecoded parameter-addressing byte no project has ever
269 /// sent as non-zero.
270 /// - "status_ext": create_get_status_extended() at the field-observed selector 0x80,
271 /// `index` = block/N (Q1). The selector-0x20 tilt control is create_get_status_tilt()
272 /// (already shipped) and is not part of this probe.
273 /// - "status_ext_fn6" / "status_ext_fn9": create_get_status_extended() at selector 0x80 with
274 /// function ID 0x06 / 0x09 instead of the usual 0x03, `index` = block/N. The 4-byte shape
275 /// is field-observed; the function ID in it is not.
276 /// - "get_info1": create_get_info1() (CMD_GET_INFO1 0x54, no payload), `index` ignored.
277 /// - "get_info2": create_get_info2() (CMD_GET_INFO2 0x56, no payload), `index` ignored.
278 /// - "general_info3": create_general_info3(), `index` ignored (Q2).
279 /// - "private2": create_private2_read() long form, `index` = modifier (Q3).
280 /// - "private2_short": create_private2_read() short form, `index` = modifier (Q3).
281 /// There is deliberately no "unknown4a" probe and no builder for CMD_UNKNOWN4A_REQ (0x4A)
282 /// anywhere in this codebase — see ADR 0024 for the standing safety decision.
283 ///
284 /// `index` is a string, not an int, because ManagementServiceDescriptor::
285 /// encode_list_service_response() hardcodes every native-API action argument as
286 /// SERVICE_ARG_TYPE_STRING (see the .cpp) — parsed here via parse_probe_index(), which
287 /// accepts both "6" and "0x06".
288 /// @param device_id Target device ID (hex string, case-insensitive).
289 /// @param probe One of the probe names above.
290 /// @param index Function ID / selector block / modifier, depending on `probe`; a decimal or
291 /// `0x`-prefixed hex string. Ignored for "general_info3".
292 /// @return Structured result whose `message` carries the attempt count, and on a reply, the
293 /// response command byte and full raw hex.
294 ManagementActionResult probe_device(const std::string &device_id, const std::string &probe, const std::string &index);
295
296 /// Native API callback: run a bounded probe sweep and publish the result as a HA event.
297 void api_probe_sweep(const std::string &device_id, const std::string &probe, const std::string &first_index,
298 const std::string &last_index);
299
300 /// @brief Walk a bounded index range, one probe_device() call per index, in one user gesture.
301 ///
302 /// Answers Q0/Q1/Q3 (the multi-value probes) without one native-API call per value. Bounded to
303 /// PROBE_SWEEP_MAX_INDICES indices and spaced PROBE_SWEEP_DELAY_MS apart — this transmits on a
304 /// shared ISM band to what may be a battery device, so a tight loop is antisocial and would
305 /// also keep the device awake and skew results. Each index costs up to
306 /// EXCHANGE_RETRY_COUNT frames (proto_timing.h), same as a single probe_device() call.
307 ///
308 /// @note This blocks the ESPHome loop (API, other components, OTA) for the whole sweep, same
309 /// blocking-exchange model as every other management action (ADR 0013) — but a full
310 /// sweep multiplies it: worst case (every index silent) is roughly
311 /// `PROBE_SWEEP_MAX_INDICES * (EXCHANGE_RETRY_COUNT * exchange_start_response_wait_ms +
312 /// (EXCHANGE_RETRY_COUNT-1) * EXCHANGE_RETRY_DELAY_MS + PROBE_SWEEP_DELAY_MS)`, on the
313 /// order of a minute at default tuning and longer if `exchange_start_response_wait_ms`
314 /// is raised. Accepted deliberately for this maintainer-triggered, explicitly-opted-in
315 /// diagnostic (`diagnostic_probes: true`) rather than restructured into scheduled steps — see
316 /// docs/diagnostic-probes.md, which states this to the
317 /// user rather than leaving it to be discovered as a frozen dashboard.
318 /// @param device_id Target device ID (hex string, case-insensitive).
319 /// @param probe One of probe_device()'s probe names.
320 /// @param first_index First index in the sweep (inclusive), same string format as probe_device().
321 /// @param last_index Last index in the sweep (inclusive).
322 /// @return Structured result whose `message` is one line per index: answered / error-coded /
323 /// silent / refused.
324 ManagementActionResult probe_sweep(const std::string &device_id, const std::string &probe,
325 const std::string &first_index, const std::string &last_index);
326
327 /// Native API callback: run a heating/climate function and publish the result as a HA event.
328 void api_heating_control(const std::string &device_id, const std::string &function, const std::string &value);
329
330 /// @brief Send one 2W heating/climate function (CMD_WRITE_PRIVATE 0x20) to a registered climate
331 /// device.
332 ///
333 /// A real user feature, but an unvalidated one — the protocol is derived from the iohomecontrol
334 /// project's Cozytouch support and has never been exercised against real Atlantic/Thermor/Sauter
335 /// hardware (see docs/configuration/climate.md's experimental banner). It is NOT diagnostic-gated;
336 /// the guard is documentation, not `diagnostic_probes:`.
337 ///
338 /// `function` is one of `power_on` / `set_temperature` / `set_mode` / `set_presence` /
339 /// `set_window` / `midnight_sync`. `value` is parsed per function: a float in degrees Celsius
340 /// (7.0-28.0) for `set_temperature`, `auto|manual|prog|off` for `set_mode`, `on|off` for
341 /// `set_presence`, `open|close` for `set_window`, ignored for the other two. A malformed value
342 /// is reported, never coerced.
343 ///
344 /// Delegates to IOHomeControlComponent::send_heating_command() so the entity and the action
345 /// share exactly one transmit path (see that method). `verified` is always false: the `set_*`
346 /// functions are write-only — nothing decodes what the radiator did into an entity. (`power_on`
347 /// and `midnight_sync` are register reads; their ACK payload is logged at DEBUG but not decoded.)
348 /// @param device_id Target device ID (hex string, case-insensitive).
349 /// @param function Heating function name (see above).
350 /// @param value Function-specific value string (see above).
351 /// @return Structured result describing success and any validation/exchange failure.
352 ManagementActionResult heating_control(const std::string &device_id, const std::string &function,
353 const std::string &value);
354
355 /// Publish a management result as one or more structured log lines (one call per line of
356 /// `result.message`, see log_multiline_result() in log_helpers.h) and a Home Assistant event.
357 void publish_result(const ManagementActionResult &result);
358
359 private:
360 /// @brief Shared preamble for every management action: validate the hub and device ID,
361 /// then resolve the target device.
362 ///
363 /// Initializes `result` with the action name and normalized device ID, then checks (in
364 /// order) that the hub is initialized, that `device_id` is a well-formed 6-hex-digit ID,
365 /// and that it names a device registered on this hub. On any failure, sets `result.message`
366 /// to a stable, caller-facing string and returns nullptr; callers should return `result`
367 /// immediately in that case.
368 /// @param action Action name to record on `result` (e.g. MANAGEMENT_ACTION_RENAME_DEVICE).
369 /// @param device_id Requested device ID (hex string, case-insensitive, as received from HA).
370 /// @param result Populated with the action name and normalized device ID, plus an error
371 /// message on failure.
372 /// @return Pointer to the resolved device, or nullptr if any validation step failed.
373 IoDevice *resolve_device_(const char *action, const std::string &device_id, ManagementActionResult &result);
374
375 /// @brief Send an authenticated request and report a stock timeout failure on no response.
376 ///
377 /// Shared by every action that talks to the device directly (rename, identify): both send
378 /// their built request via ExchangeEngine::send_and_receive() and, on failure, log the same
379 /// exchange debug snapshot and report the same "no valid response to <action_verb> request"
380 /// message shape. Actions that don't talk to the radio directly (force-open, which queues
381 /// through the hub's cover-command dispatch path instead) have no use for this helper.
382 /// @param request Frame to send.
383 /// @param response Populated with the device's reply on success.
384 /// @param action_verb Bare verb used only in the failure message, e.g. "rename" or "identify".
385 /// @param result Result to update with `message` on failure; `result.device_id` must already be
386 /// set (via resolve_device_()) since it is used for the debug log.
387 /// @return true if a response was received; false on timeout (result.message set, debug
388 /// snapshot logged).
389 bool send_authenticated_request_(const IoFrame &request, IoFrame &response, const char *action_verb,
390 ManagementActionResult &result);
391
392 const uint8_t *node_id_;
393 const uint8_t *system_key_;
394 const TuningConfig *tuning_;
395 ExchangeEngine &engine_;
396 DeviceRegistry &registry_;
397 const bool *initialized_;
399};
400
401} // namespace home_io_control
402} // namespace esphome
Owns the per-hub device table, update callbacks, and linked-remote associations.
The main IO-Homecontrol component.
Definition hub_core.h:91
ManagementActions(const ManagementActions &)=delete
Non-copyable — stores references and pointers into hub member addresses.
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.
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.
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.
ManagementActionResult scan_paired_devices()
Broadcast a roll-call and report every device that answers.
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.
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_oneway_remove_controller(const std::string &controller_id)
Native API callback: queue a standalone 1W un-enrollment (remove-controller, CMD 0x39) for a controll...
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.
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_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.
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.
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.
ManagementActions & operator=(const ManagementActions &)=delete
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.
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:93
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.
bool has_response_cmd
True when response_cmd contains a probe reply's command byte.
std::string probe_index
Requested index argument, as received (before parsing).
uint8_t result_code
Optional CMD_ERROR_RESP result byte.
std::string action
Action name, e.g. "rename_device".
uint8_t response_cmd
Probe reply's command byte (distinct from a CMD_ERROR_RESP result_code above, which is a different by...
bool terminal_refusal
True when probe_device() failed for a reason that will recur identically for every remaining index in...
std::string applied_name
Verified cached UTF-8 name after a readback, when available.
std::string probe_name
Probe name for probe_device()/probe_sweep(), e.g. "private_fn".
std::string response_hex
Probe reply's full raw wire hex, for pasting into scripts/corpus/ingest.py.
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.