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