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