Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
diagnostic_probes.h
Go to the documentation of this file.
1#pragma once
2
3/// @file diagnostic_probes.h
4/// @brief Opt-in diagnostic probes: single frames and bounded sweeps for protocol research.
5/// @ingroup hioc_hub
6///
7/// DiagnosticProbes owns probe_device(), probe_sweep() and the data-transport read behind the
8/// `read_object` probe, with the table of probe names and frame builders. ManagementActions owns
9/// one instance and registers the two native API actions that drive it; the actions exist only
10/// with `diagnostic_probes: true` (docs/diagnostic-probes.md, ADR 0024).
11///
12/// The class reaches the hub through DiagnosticProbesHub, three injected calls, and includes no
13/// hub header. None of the three applies a status to a device record, so a probe reply stays
14/// raw: it can never be misread as a position update.
15
16#include "device_registry.h"
17#include "exchange_engine.h"
18#include "management_helpers.h"
19#include "proto_constants.h"
20#include "proto_frame.h"
21
22#include <cstdint>
23#include <functional>
24#include <string>
25
26namespace esphome {
27namespace home_io_control {
28
29class RadioDriver;
30
31/// Native API action names of the two probe actions, shared with ManagementActions' registration table.
32inline constexpr const char *MANAGEMENT_ACTION_PROBE_DEVICE = "probe_device";
33inline constexpr const char *MANAGEMENT_ACTION_PROBE_SWEEP = "probe_sweep";
34
35/// @brief The only three things a probe needs from the hub, injected by ManagementActions.
37 std::function<bool()> probes_enabled; ///< True when `diagnostic_probes: true` was configured.
38 std::function<RadioDriver *()> radio; ///< The radio, for the structured capture log line.
39 std::function<bool(const IoFrame &)> record_reply_event; ///< Records a device event report on the "Last Device
40 ///< Event" sensor; true when the frame was one.
41};
42
43/// @brief Sends diagnostic probe frames to already-paired devices and reports the raw replies.
44///
45/// Non-copyable for the same reason as ManagementActions: it holds references into hub members.
46/// @ingroup hioc_hub
48 public:
49 /// Construct with the collaborators a probe exchange needs.
50 /// @param node_id Controller 3-byte node ID, owned by the hub.
51 /// @param engine Shared exchange engine for radio transactions.
52 /// @param registry Device registry for device lookups.
53 /// @param initialized Pointer to the hub's initialized flag.
54 /// @param hub The three injected hub calls.
55 DiagnosticProbes(const uint8_t *node_id, ExchangeEngine &engine, DeviceRegistry &registry, const bool *initialized,
57
60
61 /// @brief Send a single diagnostic probe frame to an already-paired device and report the raw
62 /// reply.
63 ///
64 /// Protocol-research instrumentation for opcodes this codebase has not decoded — see
65 /// docs/diagnostic-probes.md and ADR 0024. Refuses unless
66 /// this build was configured with `diagnostic_probes: true`
67 /// (DiagnosticProbesHub::probes_enabled), and separately refuses while the
68 /// target device is moving (`!effective_is_stopped(*dev)`) — an unknown frame into a
69 /// mid-transaction device state machine is the one avoidable way a read-shaped probe could
70 /// cause harm. That is the device's last *reported* movement state *or* a standing hub
71 /// prediction that it is moving (a command the hub just issued, not yet confirmed): both
72 /// warrant the same refusal. It is always "stopped" (never refuses) for a light/switch, and the
73 /// reported state can be stale if the device was last moved from a physical remote the hub
74 /// never saw. Routes the response straight into `result.message` as raw hex; it is never passed
75 /// to the hub's status decoder (this class holds no hub pointer, only the three calls of
76 /// DiagnosticProbesHub, and none of them applies a status), so a probe can never be misread as a
77 /// position update. `verified` is always false: the whole point is that the reply's meaning is not yet
78 /// known.
79 ///
80 /// `probe` selects the frame builder:
81 /// - "private_fn": create_private_function(), `index` = function ID (Q0).
82 /// - "private_fn_sub": create_private_function() at function ID 0x09, `index` = the second
83 /// payload byte (data[1]) — an undecoded parameter-addressing byte no project has ever
84 /// sent as non-zero.
85 /// - "status_ext": create_get_status_extended() at the field-observed selector 0x80,
86 /// `index` = block/N (Q1). The selector-0x20 tilt control is create_get_status_tilt()
87 /// (already shipped) and is not part of this probe.
88 /// - "status_ext_fn6" / "status_ext_fn9": create_get_status_extended() at selector 0x80 with
89 /// function ID 0x06 / 0x09 instead of the usual 0x03, `index` = block/N. The 4-byte shape
90 /// is field-observed; the function ID in it is not.
91 /// - "get_info1": create_get_info1() (CMD_GET_INFO1 0x54, no payload), `index` ignored.
92 /// - "get_info2": create_get_info2() (CMD_GET_INFO2 0x56, no payload), `index` ignored.
93 /// - "general_info3": create_general_info3(), `index` ignored (Q2).
94 /// - "private2": create_private2_read() long form, `index` = modifier (Q3).
95 /// - "private2_short": create_private2_read() short form, `index` = modifier (Q3).
96 /// - "status_mp_fp": create_status_mp_fp_read() (CMD_PRIVATE function 0x01, FPI1 mask 0xFE:
97 /// main parameter plus FP1-FP7), `index` ignored. The reply is reported raw like every
98 /// probe reply and never applied to the device record (ADR 0024).
99 /// - "status_fp_high": the same request with FPI1 0x00 (main parameter only) and `index` as the FPI2
100 /// bitmap (bit 7 = FP9 ... bit 0 = FP16, 1 to 7 bits set; anything else is rejected). Not sweepable, an
101 /// index is a bitmap. The FPI2 variant has not been seen on air; the reply is reported raw and never applied.
102 /// - "limitation": create_limitation_status_read() (CMD_LIMITATION_STATUS_REQ 0x25), `index` 0 =
103 /// resulting minimum, 1 = resulting maximum (any other index is rejected, a sweep reaching
104 /// past 1 is rejected whole). The reply is reported raw plus a decode labelled "assumed
105 /// layout" in `ManagementActionResult::decoded`; it is never applied to the device record.
106 /// - "subscribe_event": create_subscribe_event() (CMD_SUBSCRIBE_EVENT 0x73), `index` 0 = battery
107 /// level event 0x2001, 1 = local sensor event 0x2005 (any other index is rejected). It changes
108 /// device state: it stores a subscription with no confirmed way to remove it, so it is not
109 /// sweepable and the builder accepts only those two events (ADR 0060). It is sent
110 /// once: a try the device authenticated is never repeated, so silence after acceptance is
111 /// reported as "accepted but no response". Whatever the device reports afterwards is recorded
112 /// by the device-event recorder, never applied to the device record.
113 /// - "unsubscribe_event": create_unsubscribe_event(), the same request with action byte 0x00,
114 /// same index mapping and the same guards. An untested guess at undoing a subscription; whether
115 /// it works shows only in whether the device's reports stop.
116 /// - "read_object": the data-transport read of ADR 0062. `index` is a row of TRANSFER_READABLE_OBJECTS:
117 /// 0 = actuator capacity record (object 0x8100), 1 = actuator state record (0x8103), 2 = name record
118 /// (0x8003, there to prove the flow before the other two are trusted), 3 = general info 1 (0x8001),
119 /// 4 = general info 2 (0x8002), 5 to 29 = further class-0 and Somfy manufacturer objects. One call
120 /// opens the object (CMD_TRANSFER_OPEN), pulls its blocks (CMD_UNKNOWN4A_REQ, counting down) and closes it; the
121 /// bytes are reported raw in `response_hex` and logged, never interpreted or applied to the device record. Not
122 /// sweepable; an object announced longer than PROBE_READ_OBJECT_MAX_BYTES is closed unread. The write twin of the
123 /// block (0x48, and a 0x4A that carries data) has no builder.
124 /// - "register_00" / "register_01": a read of register `0x00<index>` / `0x01<index>` over CMD_WRITE_PRIVATE
125 /// (`0C 60 <hi> <lo>`, ADR 0063). The direction byte is the read constant and cannot be set to the write
126 /// direction by any input (encode_heating_register_read()). Refused for a device that is neither a
127 /// register-protocol heater nor a domestic hot water device (device_accepts_register_probe()); sweepable.
128 /// A `0x21` that echoes the register also reports its value bytes raw in `decoded`. Family `0x00xx` is
129 /// untested; `0x01xx` is the one climate support reads.
130 ///
131 /// `index` is a string, not an int, because ManagementServiceDescriptor::
132 /// encode_list_service_response() hardcodes every native-API action argument as
133 /// SERVICE_ARG_TYPE_STRING (see management_actions.cpp) — parsed here via parse_probe_index()
134 /// (diagnostic_probes.cpp), which accepts both "6" and "0x06".
135 /// @param device_id Target device ID (hex string, case-insensitive).
136 /// @param probe One of the probe names above.
137 /// @param index Function ID / selector block / modifier, depending on `probe`; a decimal or
138 /// `0x`-prefixed hex string. Ignored for "general_info3".
139 /// @return Structured result whose `message` carries the attempt count, and on a reply, the
140 /// response command byte and full raw hex.
141 ManagementActionResult probe_device(const std::string &device_id, const std::string &probe, const std::string &index);
142
143 /// @brief Walk a bounded index range, one probe_device() call per index, in one user gesture.
144 ///
145 /// Answers Q0/Q1/Q3 (the multi-value probes) without one native-API call per value. Bounded to
146 /// PROBE_SWEEP_MAX_INDICES indices and spaced PROBE_SWEEP_DELAY_MS apart — this transmits on a
147 /// shared ISM band to what may be a battery device, so a tight loop is antisocial and would
148 /// also keep the device awake and skew results. Each index costs up to
149 /// EXCHANGE_RETRY_COUNT frames (proto_timing.h), same as a single probe_device() call.
150 ///
151 /// @note This blocks the ESPHome loop (API, other components, OTA) for the whole sweep, same
152 /// blocking-exchange model as every other management action (ADR 0013) — but a full
153 /// sweep multiplies it: worst case (every index silent) is roughly
154 /// `PROBE_SWEEP_MAX_INDICES * (EXCHANGE_RETRY_COUNT * exchange_start_response_wait_ms +
155 /// (EXCHANGE_RETRY_COUNT-1) * EXCHANGE_RETRY_DELAY_MS + PROBE_SWEEP_DELAY_MS)`, on the
156 /// order of a minute at default tuning and longer if `exchange_start_response_wait_ms`
157 /// is raised. Accepted deliberately for this maintainer-triggered, explicitly-opted-in
158 /// diagnostic (`diagnostic_probes: true`) rather than restructured into scheduled steps — see
159 /// docs/diagnostic-probes.md, which states this to the
160 /// user rather than leaving it to be discovered as a frozen dashboard.
161 /// @param device_id Target device ID (hex string, case-insensitive).
162 /// @param probe One of probe_device()'s probe names.
163 /// @param first_index First index in the sweep (inclusive), same string format as probe_device().
164 /// @param last_index Last index in the sweep (inclusive).
165 /// @return Structured result whose `message` is one line per index: answered / error-coded /
166 /// silent / refused.
167 ManagementActionResult probe_sweep(const std::string &device_id, const std::string &probe,
168 const std::string &first_index, const std::string &last_index);
169
170 private:
171 /// @brief Second half of the `read_object` probe: given the device's accepted CMD_TRANSFER_OPEN
172 /// answer, pull every block, close the transfer and report what was read.
173 ///
174 /// Always closes a transfer the device opened, also after a failed block, so the device is not left
175 /// holding an open handle. Writes the outcome into `result` (`response_hex`, `decoded`, `message`);
176 /// the bytes are never decoded further and never reach the device record.
177 /// @param dev The probed device.
178 /// @param open_reply The device's answer to the open; anything but a CMD_TRANSFER_OPEN_RESP is left to
179 /// the caller's own report and ends the read here.
180 /// @param object The object that was opened (for the log and the message).
181 /// @param result Receives the outcome appended to the first-exchange text already in it.
182 void read_object_blocks_(const IoDevice &dev, const IoFrame &open_reply, const TransferObject &object,
183 ManagementActionResult &result);
184
185 const uint8_t *node_id_;
186 ExchangeEngine &engine_;
187 DeviceRegistry &registry_;
188 const bool *initialized_;
190};
191
192} // namespace home_io_control
193} // namespace esphome
Owns the per-hub device table, update callbacks, and linked-remote associations.
DiagnosticProbes(const uint8_t *node_id, ExchangeEngine &engine, DeviceRegistry &registry, const bool *initialized, DiagnosticProbesHub hub)
Construct with the collaborators a probe exchange needs.
DiagnosticProbes(const DiagnosticProbes &)=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.
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.
DiagnosticProbes & operator=(const DiagnosticProbes &)=delete
Abstract radio driver for IO-Homecontrol.
Per-hub device table, update-callback fan-out, and linked-remote map.
Self-contained authenticated exchange engine for IO-Homecontrol 2W.
Result type and argument/result helpers shared by ManagementActions and DiagnosticProbes.
constexpr const char * MANAGEMENT_ACTION_PROBE_DEVICE
Native API action names of the two probe actions, shared with ManagementActions' registration table.
constexpr const char * MANAGEMENT_ACTION_PROBE_SWEEP
IO-Homecontrol command IDs, result codes and protocol enumerations.
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
The only three things a probe needs from the hub, injected by ManagementActions.
std::function< bool(const IoFrame &)> record_reply_event
Records a device event report on the "Last Device Event" sensor; true when the ...
std::function< bool()> probes_enabled
True when diagnostic_probes: true was configured.
std::function< RadioDriver *()> radio
The radio, for the structured capture log line.
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.
One object a CMD_TRANSFER_OPEN can name: its class byte and its 16-bit id.