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`/`clear_optimistic_target` may fire.
32};
33
34/// Owns the per-hub device table, update callbacks, and linked-remote associations.
35///
36/// All of the hub's add/get/subscribe/notify operations go through this class —
37/// IOHomeControlComponent holds no device state of its own. The class has no
38/// ESPHome dependencies beyond logging and is directly host-testable.
39/// @ingroup hioc_hub
41 public:
42 /// Register a device by ID with default metadata (UNKNOWN type, subtype 0, not inverted).
43 /// No-op when @p device_id is already registered. Warns and returns when the hex string is invalid.
44 /// @param device_id Hexadecimal node ID string (e.g. "ABC123").
45 void add(const std::string &device_id);
46
47 /// Register a device with full YAML-derived metadata.
48 /// No-op when @p device_id is already registered. Warns and returns when the hex string is invalid.
49 /// @param device_id Hexadecimal node ID string.
50 /// @param cfg Device type/subtype/inversion/optimistic-state metadata.
51 void add(const std::string &device_id, const DeviceConfig &cfg);
52
53 /// Insert or overwrite a device entry without deduplication or hex-validation checks.
54 /// Used by the pairing flow which already validated the device during discovery.
55 /// @param device_id Hexadecimal node ID string.
56 /// @param device Fully-built device to store.
57 void put(const std::string &device_id, IoDevice device);
58
59 /// Retrieve a registered device by ID.
60 /// @return Pointer to the stored IoDevice, or nullptr when not found.
61 [[nodiscard]] IoDevice *get(const std::string &device_id);
62
63 /// Set a device's `dimmable` flag (see IoDevice::dimmable). No-op when the device is unknown.
64 /// Called by platform_light.cpp's setup() right after registration, since `dimmable` is a
65 /// light-platform YAML choice, not something add()'s shared cover/light/switch/lock signature
66 /// should carry for every entity type.
67 /// @param device_id Device to update.
68 /// @param dimmable New value for IoDevice::dimmable.
69 void set_dimmable(const std::string &device_id, bool dimmable);
70
71 /// Register a callback that fires whenever a device's state changes.
72 /// @param cb Callable with signature void(const std::string &device_id, const IoDevice &device).
74
75 /// Invoke all registered callbacks for @p device_id.
76 /// No-op when @p device_id is not in the registry.
77 /// @param device_id Device whose state just changed.
78 void notify(const std::string &device_id);
79
80 /// Record that a remote node controls a registered device.
81 /// When activity from the remote is overheard, a status poll is scheduled for the linked device.
82 /// @param remote_id Node ID of the remote control.
83 /// @param device_id Node ID of the device it controls.
84 void add_linked_remote(const std::string &remote_id, const std::string &device_id);
85
86 /// Retrieve the list of device IDs linked to a remote.
87 /// @return Pointer to the device-ID list, or nullptr when the remote is unknown.
88 [[nodiscard]] const std::vector<std::string> *linked_devices(const std::string &remote_id) const;
89
90 /// Record that a remote's typed-broadcast presses (e.g. "all awnings") should also apply to
91 /// @p device_id, matching how 1W remotes address a device class rather than a single node.
92 /// Independent of add_linked_remote()'s id-keyed map — a device may be linked both ways;
93 /// callers dedup (see IOHomeControlComponent's 1W dispatch) so it is only touched once per press.
94 /// @param type Device class the broadcast targets.
95 /// @param device_id Node ID of the device to add to that class.
96 void add_linked_remote_class(DeviceType type, const std::string &device_id);
97
98 /// Retrieve the list of device IDs linked to a device class.
99 /// @return Pointer to the device-ID list, or nullptr when no device is linked to that class.
100 [[nodiscard]] const std::vector<std::string> *linked_devices_for_class(DeviceType type) const;
101
102 /// Set an optimistic target position ahead of a confirming poll/response, and notify.
103 ///
104 /// No-op (and returns false) when the device is unknown or has `optimistic_state == false`.
105 /// Never touches `position` — only the caller's later poll/response settles that. Used by
106 /// both the 1W linked-remote path and HA-issued 2W cover commands so the entity shows
107 /// movement direction immediately instead of only after the confirming update arrives.
108 /// @param device_id Device to update.
109 /// @param target_io_position Target position in IO units (0=open, 100=closed).
110 /// @return true if the optimistic state was applied.
111 bool apply_optimistic_target(const std::string &device_id, float target_io_position);
112
113 /// Clear a device's optimistic target (e.g. on STOP), and notify.
114 ///
115 /// No-op (and returns false) when the device is unknown or has `optimistic_state == false`.
116 /// @param device_id Device to update.
117 /// @return true if the optimistic target was cleared.
118 bool clear_optimistic_target(const std::string &device_id);
119
120 /// Set an optimistic slat angle ahead of a confirming poll, and notify.
121 ///
122 /// No-op (and returns false) when the device is unknown, has `optimistic_state == false`, or is
123 /// not a tilt-capable type. Nothing else can fill the gap: unlike a position command, a tilt
124 /// command's own reply carries no slat angle this hub can use. The EXECUTE ack lays its payload
125 /// out differently from a status reply and is not decoded for tilt at all (see the offset
126 /// constants in hub_status.cpp and
127 /// tests/corpus/captures/issues/issue_60_tilt_execute_ack_tilt_block*.yaml), so without this the
128 /// entity would keep showing the pre-command angle until the next status poll seconds later.
129 ///
130 /// Deliberately does not touch `is_stopped`, unlike apply_optimistic_target(): the HA movement
131 /// animation is derived from main-position delta, and a tilt-only command does not move the
132 /// main position — marking the device as moving would animate an open/close that is not
133 /// happening. The EXECUTE ack settles `is_stopped` from the wire a fraction of a second later.
134 /// @param device_id Device to update.
135 /// @param tilt_percent Slat angle in the same percent scale as `IoDevice::tilt` (0-100).
136 /// @return true if the optimistic tilt was applied.
137 bool apply_optimistic_tilt(const std::string &device_id, float tilt_percent);
138
139 /// @return Number of registered devices.
140 [[nodiscard]] size_t size() const { return devices_.size(); }
141
142 /// @return Number of distinct linked-remote entries.
143 [[nodiscard]] size_t linked_remote_count() const { return linked_remotes_.size(); }
144
145 /// Invoke @p fn(remote_id, device_id_list) for every linked-remote entry.
146 /// @param fn Callable receiving the remote ID and its associated device ID list.
148 const std::function<void(const std::string &, const std::vector<std::string> &)> &fn) const;
149
150 /// Mutable begin iterator over (device_id, IoDevice) pairs (supports range-for in the poll loop).
151 std::map<std::string, IoDevice>::iterator begin() { return devices_.begin(); }
152 /// Mutable end iterator over (device_id, IoDevice) pairs.
153 std::map<std::string, IoDevice>::iterator end() { return devices_.end(); }
154
155 private:
156 std::map<std::string, IoDevice> devices_;
157 std::vector<DeviceUpdateCallback> callbacks_;
158 std::map<std::string, std::vector<std::string>> linked_remotes_;
159 std::map<DeviceType, std::vector<std::string>> linked_remote_classes_;
160};
161
162} // namespace home_io_control
163} // 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.
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.
const std::vector< std::string > * linked_devices_for_class(DeviceType type) const
Retrieve the list of device IDs linked to a device class.
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 notify(const std::string &device_id)
Invoke all registered callbacks for device_id.
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.
bool clear_optimistic_target(const std::string &device_id)
Clear a device's optimistic target (e.g.
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).
DeviceType
Device type identifiers reported by IO‑Homecontrol products.
@ UNKNOWN
Unknown/unspecified device.
std::function< void(const std::string &device_id, const IoDevice &device)> DeviceUpdateCallback
Callback type invoked when a device's state changes.
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/clear_optimistic_target may fire.
bool inverted
Position-inversion flag.
uint8_t subtype
Device subtype byte.
DeviceType type
Device type from YAML declaration.
Runtime state of a paired IO‑Homecontrol device.