Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
device_registry.h
Go to the documentation of this file.
1#pragma once
2
3/// @file device_registry.h
4/// @brief Per-hub device table, update-callback fan-out, and linked-remote map.
5/// @ingroup hioc_hub
6///
7/// DeviceRegistry owns the per-hub device table, the update-callback list, and the
8/// linked-remote map. Keeping them in one class lets the polling loop, status
9/// engine, and pairing flow share a single, testable source of truth without
10/// knowing about each other.
11
12#include "proto_device_model.h"
13
14#include <functional>
15#include <map>
16#include <optional>
17#include <string>
18#include <vector>
19
20namespace esphome {
21namespace home_io_control {
22
23/// Callback type invoked when a device's state changes.
24using DeviceUpdateCallback = std::function<void(const std::string &device_id, const IoDevice &device)>;
25
26/// YAML-declared device metadata for registration; defaults match an undeclared device.
27/// A new per-device attribute is a new field with a default here, not a new `add()` overload.
29 DeviceType type{DeviceType::UNKNOWN}; ///< Device type from YAML declaration.
30 uint8_t subtype{0}; ///< Device subtype byte.
31 DeviceModel model{DeviceModel::NONE}; ///< YAML `io_model:`; NONE when not declared.
33 DeviceCapabilityClass::UNKNOWN}; ///< Class of the declaring entity; decides
34 ///< the features of a kind the type table does not know.
35 std::optional<bool> invert_override =
36 std::nullopt; ///< YAML `invert_position:` when declared; the kind's default otherwise.
37 bool dimmable{false}; ///< YAML `dimmable: true` of a light.
38 bool optimistic_state{true}; ///< Whether `apply_optimistic_target`/`apply_optimistic_stop` may fire.
39 TravelSpeed travel_speed{TravelSpeed::FAST}; ///< Speed profile of position moves.
40 bool low_power{false}; ///< Target is a low-power / duty-cycled receiver: directed frames set
41 ///< CTRL1_LOW_POWER and use the long wake-up preamble.
42 bool battery_entities{false}; ///< `battery: true` declared: the battery entities exist for this device.
43 bool has_system_key{false}; ///< `system_key:` declared: the device keeps a key of its own.
44 uint8_t system_key[AES_KEY_SIZE]{}; ///< That key; meaningful only when `has_system_key`. Never logged.
45};
46
47/// Owns the per-hub device table, update callbacks, and linked-remote associations.
48///
49/// All of the hub's add/get/subscribe/notify operations go through this class —
50/// IOHomeControlComponent holds no device state of its own. The class has no
51/// ESPHome dependencies beyond logging and is directly host-testable.
52/// @ingroup hioc_hub
54 public:
55 /// Register a device by ID with default metadata (UNKNOWN type, subtype 0, no overrides).
56 /// No-op when @p device_id is already registered. Warns and returns when the hex string is invalid.
57 /// @param device_id Hexadecimal node ID string (e.g. "ABC123").
58 void add(const std::string &device_id);
59
60 /// Register a device with full YAML-derived metadata.
61 /// No-op when @p device_id is already registered. Warns and returns when the hex string is invalid.
62 /// @param device_id Hexadecimal node ID string.
63 /// The record's features are resolved from the declared kind and overrides (resolve_device_features()).
64 /// @param cfg Device type/subtype/overrides/optimistic-state metadata.
65 void add(const std::string &device_id, const DeviceConfig &cfg);
66
67 /// Insert or overwrite a device entry without deduplication or hex-validation checks.
68 /// Used by the pairing flow which already validated the device during discovery. A record already
69 /// stored under the ID hands its YAML overrides (entity class, `invert_position:`, `dimmable:`, `io_model:`) to
70 /// the new one, and the new record's features are resolved before it is stored.
71 /// @param device_id Hexadecimal node ID string.
72 /// @param device Fully-built device to store.
73 void put(const std::string &device_id, IoDevice device);
74
75 /// Retrieve a registered device by ID.
76 /// @return Pointer to the stored IoDevice, or nullptr when not found.
77 [[nodiscard]] IoDevice *get(const std::string &device_id);
78
79 /// Retrieve a device the YAML declares, ignoring one a pairing added in this boot.
80 ///
81 /// A device a pairing registered (IoDevice::registered_by_pairing) is controllable until reboot but
82 /// still needs its YAML block, so roll-call reports treat it as unknown and a later pairing may
83 /// replace its record.
84 /// @return Pointer to the stored IoDevice, or nullptr when not found or added by a pairing.
85 [[nodiscard]] IoDevice *get_declared(const std::string &device_id);
86
87 /// Remove a device a pairing added in this boot. A device the YAML declares is never removed.
88 /// @param device_id Hexadecimal node ID string.
89 /// @return true when a record was removed.
90 bool remove_registered_by_pairing(const std::string &device_id);
91
92 /// Select a device's travel speed. A declared preference with no protocol readback — nothing on
93 /// the wire reports which speed a device is moved with.
94 /// @param device_id Hexadecimal node ID string.
95 /// @param speed Speed profile of the device's position moves.
96 void set_travel_speed(const std::string &device_id, TravelSpeed speed);
97
98 /// Register a callback that fires whenever a device's state changes.
99 /// @param cb Callable with signature void(const std::string &device_id, const IoDevice &device).
101
102 /// Invoke all registered callbacks for @p device_id.
103 /// No-op when @p device_id is not in the registry.
104 /// @param device_id Device whose state just changed.
105 void notify(const std::string &device_id);
106
107 /// Record that a remote node controls a registered device.
108 /// When activity from the remote is overheard, a status poll is scheduled for the linked device.
109 /// @param remote_id Node ID of the remote control.
110 /// @param device_id Node ID of the device it controls.
111 void add_linked_remote(const std::string &remote_id, const std::string &device_id);
112
113 /// Retrieve the list of device IDs linked to a remote.
114 /// @return Pointer to the device-ID list, or nullptr when the remote is unknown.
115 [[nodiscard]] const std::vector<std::string> *linked_devices(const std::string &remote_id) const;
116
117 /// Record that a remote's typed-broadcast presses (e.g. "all awnings") should also apply to
118 /// @p device_id, matching how 1W remotes address a device class rather than a single node.
119 /// Independent of add_linked_remote()'s id-keyed map — a device may be linked both ways;
120 /// callers dedup (see IOHomeControlComponent's 1W dispatch) so it is only touched once per press.
121 /// @param type Device class the broadcast targets.
122 /// @param device_id Node ID of the device to add to that class.
123 void add_linked_remote_class(DeviceType type, const std::string &device_id);
124
125 /// Retrieve the list of device IDs linked to a device class.
126 /// @return Pointer to the device-ID list, or nullptr when no device is linked to that class.
127 [[nodiscard]] const std::vector<std::string> *linked_devices_for_class(DeviceType type) const;
128
129 /// Every device with a `linked_remotes` entry, by remote id or by `class:`, once each, sorted.
130 [[nodiscard]] std::vector<std::string> all_linked_devices() const;
131
132 /// Set an optimistic target position ahead of a confirming poll/response, and notify.
133 ///
134 /// No-op (and returns false) when the device is unknown or has `optimistic_state == false`.
135 /// Never touches `position` — only the caller's later poll/response settles that. Used by
136 /// both the 1W linked-remote path and HA-issued 2W cover commands so the entity shows
137 /// movement direction immediately instead of only after the confirming update arrives.
138 /// @param device_id Device to update.
139 /// @param target_io_position Target position in IO units (0=open, 100=closed).
140 /// @return true if the optimistic state was applied.
141 bool apply_optimistic_target(const std::string &device_id, float target_io_position);
142
143 /// Predict that a device has stopped (e.g. on STOP), and notify.
144 ///
145 /// No-op (and returns false) when the device is unknown or has `optimistic_state == false`.
146 /// Records a `Motion::STOPPED` prediction rather than merely withdrawing the position
147 /// prediction: a bare withdrawal would fall back to an observed `is_stopped == false` and keep
148 /// the HA cover animating. The prediction is superseded by the next decoded status.
149 /// @param device_id Device to update.
150 /// @param restorable True for a STOP this hub is about to send: a movement prediction it
151 /// replaces is kept so rollback_optimistic() can bring it back if the STOP fails. False
152 /// for a stop that cannot fail from the hub's side (an overheard remote's STOP).
153 /// @return true if the optimistic stop was applied.
154 bool apply_optimistic_stop(const std::string &device_id, bool restorable = false);
155
156 /// Set an optimistic slat angle ahead of a confirming poll, and notify.
157 ///
158 /// No-op (and returns false) when the device is unknown, has `optimistic_state == false`, or is
159 /// not a tilt-capable type. Nothing else can fill the gap: unlike a position command, a tilt
160 /// command's own reply carries no slat angle this hub can use. The EXECUTE ack lays its payload
161 /// out differently from a status reply and is not decoded for tilt at all (see the offset
162 /// constants in hub_status.cpp and
163 /// tests/corpus/captures/exchange/tilt_cover_exchange_ack_tilt_block*.yaml), so without this the
164 /// entity would keep showing the pre-command angle until the next status poll seconds later.
165 ///
166 /// Deliberately records no movement prediction, unlike apply_optimistic_target(): the HA
167 /// movement animation is derived from main-position delta, and a tilt-only command does not move
168 /// the main position — predicting motion would animate an open/close that is not happening. The
169 /// EXECUTE ack settles `is_stopped` from the wire a fraction of a second later.
170 /// @param device_id Device to update.
171 /// @param tilt_percent Slat angle in the same percent scale as `IoDevice::tilt` (0-100).
172 /// @return true if the optimistic tilt was applied.
173 bool apply_optimistic_tilt(const std::string &device_id, float tilt_percent);
174
175 /// Withdraw every prediction for a device after the command that produced them failed, and notify.
176 ///
177 /// Self-guarding: an empty overlay means nothing was predicted (including every device configured
178 /// `optimistic_state: false`, whose apply_* calls all no-op), so there is nothing to withdraw and
179 /// no reason to republish. Never touches an observed field — the device did not move, so its last
180 /// reported position remains correct and stays on display.
181 ///
182 /// A failed restorable STOP (apply_optimistic_stop()) is the exception: the device keeps doing
183 /// what it was doing, so the movement prediction the STOP replaced comes back, and every other
184 /// prediction is withdrawn as usual. Without that the entity would fall back to an observation
185 /// that, on a device reporting nothing mid-travel, reads "moving, target = position" and shows
186 /// as idle.
187 /// @param device_id Device whose failed command's predictions should be withdrawn.
188 /// @param failed_stop True when the failed command is a STOP. Only then does the replaced
189 /// movement come back; any other failed command (e.g. a tilt run ahead of a still-queued
190 /// STOP) withdraws everything.
191 /// @return true if a prediction was withdrawn or restored.
192 bool rollback_optimistic(const std::string &device_id, bool failed_stop = false);
193
194 /// Mark a device's STOP as delivered: a later rollback must not restore the movement it
195 /// replaced. Leaves every prediction in place. No-op for an unknown device.
196 /// @param device_id Device that accepted the STOP.
197 void confirm_optimistic_stop(const std::string &device_id);
198
199 /// @return Number of registered devices.
200 [[nodiscard]] size_t size() const { return devices_.size(); }
201
202 /// @return Number of distinct linked-remote entries.
203 [[nodiscard]] size_t linked_remote_count() const { return linked_remotes_.size(); }
204
205 /// Invoke @p fn(remote_id, device_id_list) for every linked-remote entry.
206 /// @param fn Callable receiving the remote ID and its associated device ID list.
208 const std::function<void(const std::string &, const std::vector<std::string> &)> &fn) const;
209
210 /// Mutable begin iterator over (device_id, IoDevice) pairs (supports range-for in the poll loop).
211 std::map<std::string, IoDevice>::iterator begin() { return devices_.begin(); }
212 /// Mutable end iterator over (device_id, IoDevice) pairs.
213 std::map<std::string, IoDevice>::iterator end() { return devices_.end(); }
214
215 private:
216 std::map<std::string, IoDevice> devices_;
217 std::vector<DeviceUpdateCallback> callbacks_;
218 std::map<std::string, std::vector<std::string>> linked_remotes_;
219 std::map<DeviceType, std::vector<std::string>> linked_remote_classes_;
220};
221
222} // namespace home_io_control
223} // namespace esphome
Owns the per-hub device table, update callbacks, and linked-remote associations.
const std::vector< std::string > * linked_devices(const std::string &remote_id) const
Retrieve the list of device IDs linked to a remote.
std::map< std::string, IoDevice >::iterator end()
Mutable end iterator over (device_id, IoDevice) pairs.
bool rollback_optimistic(const std::string &device_id, bool failed_stop=false)
Withdraw every prediction for a device after the command that produced them failed,...
bool apply_optimistic_tilt(const std::string &device_id, float tilt_percent)
Set an optimistic slat angle ahead of a confirming poll, and notify.
IoDevice * get_declared(const std::string &device_id)
Retrieve a device the YAML declares, ignoring one a pairing added in this boot.
const std::vector< std::string > * linked_devices_for_class(DeviceType type) const
Retrieve the list of device IDs linked to a device class.
bool remove_registered_by_pairing(const std::string &device_id)
Remove a device a pairing added in this boot.
void add_linked_remote_class(DeviceType type, const std::string &device_id)
Record that a remote's typed-broadcast presses (e.g.
void put(const std::string &device_id, IoDevice device)
Insert or overwrite a device entry without deduplication or hex-validation checks.
std::map< std::string, IoDevice >::iterator begin()
Mutable begin iterator over (device_id, IoDevice) pairs (supports range-for in the poll loop).
bool apply_optimistic_target(const std::string &device_id, float target_io_position)
Set an optimistic target position ahead of a confirming poll/response, and notify.
std::vector< std::string > all_linked_devices() const
Every device with a linked_remotes entry, by remote id or by class:, once each, sorted.
void notify(const std::string &device_id)
Invoke all registered callbacks for device_id.
bool apply_optimistic_stop(const std::string &device_id, bool restorable=false)
Predict that a device has stopped (e.g.
void add_linked_remote(const std::string &remote_id, const std::string &device_id)
Record that a remote node controls a registered device.
void subscribe(DeviceUpdateCallback cb)
Register a callback that fires whenever a device's state changes.
IoDevice * get(const std::string &device_id)
Retrieve a registered device by ID.
void confirm_optimistic_stop(const std::string &device_id)
Mark a device's STOP as delivered: a later rollback must not restore the movement it replaced.
void for_each_linked_remote(const std::function< void(const std::string &, const std::vector< std::string > &)> &fn) const
Invoke fn(remote_id, device_id_list) for every linked-remote entry.
void add(const std::string &device_id)
Register a device by ID with default metadata (UNKNOWN type, subtype 0, no overrides).
void set_travel_speed(const std::string &device_id, TravelSpeed speed)
Select a device's travel speed.
std::function< void(const std::string &device_id, const IoDevice &device)> DeviceUpdateCallback
Callback type invoked when a device's state changes.
DeviceModel
A product model below a (node type, subtype): for products that report the same wire type but need di...
@ NONE
No model: the (type, subtype) row.
DeviceType
IO-Homecontrol node types.
@ UNKNOWN
Unknown/unspecified device.
DeviceCapabilityClass
High‑level capability class derived from DeviceType.
TravelSpeed
The speed a cover's position moves ask the motor for (ADR 0065).
@ FAST
Profile 0x06, what the hub sends unless a cover says otherwise.
IO-Homecontrol device-type model, capabilities and runtime device state.
YAML-declared device metadata for registration; defaults match an undeclared device.
bool optimistic_state
Whether apply_optimistic_target/apply_optimistic_stop may fire.
bool has_system_key
system_key: declared: the device keeps a key of its own.
DeviceModel model
YAML io_model:; NONE when not declared.
DeviceCapabilityClass entity_class
Class of the declaring entity; decides the features of a kind the type table does not know.
bool low_power
Target is a low-power / duty-cycled receiver: directed frames set CTRL1_LOW_POWER and use the long wa...
bool dimmable
YAML dimmable: true of a light.
uint8_t system_key[AES_KEY_SIZE]
That key; meaningful only when has_system_key. Never logged.
std::optional< bool > invert_override
YAML invert_position: when declared; the kind's default otherwise.
bool battery_entities
battery: true declared: the battery entities exist for this device.
uint8_t subtype
Device subtype byte.
TravelSpeed travel_speed
Speed profile of position moves.
DeviceType type
Device type from YAML declaration.
Runtime state of a paired IO‑Homecontrol device.