Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
device_registry.cpp
Go to the documentation of this file.
1/// @file device_registry.cpp
2/// @brief DeviceRegistry implementation.
3/// @ingroup hioc_hub
4
5#include "device_registry.h"
6
7#include "proto_frame.h"
8#include "proto_sizes.h"
9
10#include "esphome/core/log.h"
11
12#include <cstring>
13#include <set>
14
15namespace esphome {
16namespace home_io_control {
17
18static constexpr const char *TAG = "home_io_control";
19
20void DeviceRegistry::add(const std::string &device_id) { add(device_id, DeviceConfig{}); }
21
22void DeviceRegistry::add(const std::string &device_id, const DeviceConfig &cfg) {
23 if (devices_.count(device_id) != 0)
24 return;
25 IoDevice dev{};
26 if (!hex_to_bytes(device_id, dev.node_id, NODE_ID_SIZE)) {
27 ESP_LOGW(TAG, "Ignoring invalid device ID %s", device_id.c_str());
28 return;
29 }
30 dev.type = cfg.type;
31 dev.subtype = cfg.subtype;
32 dev.model = cfg.model;
33 dev.entity_class = cfg.entity_class;
35 dev.dimmable = cfg.dimmable;
37 dev.travel_speed = cfg.travel_speed;
38 dev.low_power = cfg.low_power;
41 if (cfg.has_system_key)
42 memcpy(dev.system_key, cfg.system_key, AES_KEY_SIZE);
44 devices_[device_id] = dev;
45}
46
47void DeviceRegistry::put(const std::string &device_id, IoDevice device) {
48 const auto existing = devices_.find(device_id);
49 if (existing != devices_.end()) {
50 device.entity_class = existing->second.entity_class;
51 device.invert_override = existing->second.invert_override;
52 device.dimmable = existing->second.dimmable;
53 device.model = existing->second.model;
54 }
56 devices_[device_id] = device;
57}
58
59IoDevice *DeviceRegistry::get(const std::string &device_id) {
60 auto it = devices_.find(device_id);
61 return (it != devices_.end()) ? &it->second : nullptr;
62}
63
64IoDevice *DeviceRegistry::get_declared(const std::string &device_id) {
65 auto it = devices_.find(device_id);
66 return (it != devices_.end() && !it->second.registered_by_pairing) ? &it->second : nullptr;
67}
68
69bool DeviceRegistry::remove_registered_by_pairing(const std::string &device_id) {
70 auto it = devices_.find(device_id);
71 if (it == devices_.end() || !it->second.registered_by_pairing)
72 return false;
73 devices_.erase(it);
74 return true;
75}
76
77void DeviceRegistry::set_travel_speed(const std::string &device_id, TravelSpeed speed) {
78 if (IoDevice *dev = this->get(device_id))
79 dev->travel_speed = speed;
80}
81
82void DeviceRegistry::subscribe(DeviceUpdateCallback cb) { callbacks_.push_back(std::move(cb)); }
83
84void DeviceRegistry::notify(const std::string &device_id) {
85 auto it = devices_.find(device_id);
86 if (it == devices_.end())
87 return;
88 for (auto &cb : callbacks_)
89 cb(device_id, it->second);
90}
91
92void DeviceRegistry::add_linked_remote(const std::string &remote_id, const std::string &device_id) {
93 linked_remotes_[remote_id].push_back(device_id);
94}
95
96const std::vector<std::string> *DeviceRegistry::linked_devices(const std::string &remote_id) const {
97 auto it = linked_remotes_.find(remote_id);
98 return (it != linked_remotes_.end()) ? &it->second : nullptr;
99}
100
101void DeviceRegistry::add_linked_remote_class(DeviceType type, const std::string &device_id) {
102 linked_remote_classes_[type].push_back(device_id);
103}
104
105const std::vector<std::string> *DeviceRegistry::linked_devices_for_class(DeviceType type) const {
106 auto it = linked_remote_classes_.find(type);
107 return (it != linked_remote_classes_.end()) ? &it->second : nullptr;
108}
109
110bool DeviceRegistry::apply_optimistic_target(const std::string &device_id, float target_io_position) {
111 auto it = devices_.find(device_id);
112 if (it == devices_.end() || !it->second.optimistic_state)
113 return false;
114 // Logs the raw (non-inverted) IO-protocol values the entity layer will compare against —
115 // cross-check against platform_cover.cpp::on_device_update_()'s invert-aware direction math
116 // when verifying a specific device's optimistic behavior.
117 if (it->second.position == UNKNOWN_POSITION) {
118 ESP_LOGD(TAG, "Device %s: optimistic target=%.0f (inverted=%s, last reported position=unknown)", device_id.c_str(),
119 target_io_position, YESNO(it->second.inverted));
120 } else {
121 ESP_LOGD(TAG, "Device %s: optimistic target=%.0f (inverted=%s, last reported position=%.0f)", device_id.c_str(),
122 target_io_position, YESNO(it->second.inverted), it->second.position);
123 }
124 it->second.optimistic.target = target_io_position;
125 it->second.optimistic.motion = OptimisticState::Motion::MOVING;
126 it->second.optimistic.forget_replaced_movement(); // a new command supersedes any pending STOP's
127 notify(device_id);
128 return true;
129}
130
131bool DeviceRegistry::apply_optimistic_stop(const std::string &device_id, bool restorable) {
132 auto it = devices_.find(device_id);
133 if (it == devices_.end() || !it->second.optimistic_state)
134 return false;
135 OptimisticState &optimistic = it->second.optimistic;
136 ESP_LOGD(TAG, "Device %s: optimistic stop", device_id.c_str());
137 if (!restorable) {
138 optimistic.forget_replaced_movement();
139 } else if (optimistic.motion == OptimisticState::Motion::MOVING) {
140 // A repeated STOP (motion already STOPPED) keeps the movement the first one replaced.
141 optimistic.stop_replaced_movement = true;
142 optimistic.stop_replaced_target = optimistic.target;
143 }
144 optimistic.target = UNKNOWN_POSITION;
146 notify(device_id);
147 return true;
148}
149
150bool DeviceRegistry::apply_optimistic_tilt(const std::string &device_id, float tilt_percent) {
151 auto it = devices_.find(device_id);
152 if (it == devices_.end() || !it->second.optimistic_state)
153 return false;
154 // Feature-gated here rather than at the call sites so every present and future caller is safe:
155 // showing an angle the device will never report back would be a lie the next poll cannot
156 // correct. This is the same bit known_device_accepts_execute_tilt() reads, so the two cannot
157 // disagree about a given device.
158 if (!has_feature(it->second.features, device_feature::TILT))
159 return false;
160 if (it->second.tilt == UNKNOWN_POSITION) {
161 ESP_LOGD(TAG, "Device %s: optimistic tilt=%.0f (last reported tilt=unknown)", device_id.c_str(), tilt_percent);
162 } else {
163 ESP_LOGD(TAG, "Device %s: optimistic tilt=%.0f (last reported tilt=%.0f)", device_id.c_str(), tilt_percent,
164 it->second.tilt);
165 }
166 it->second.optimistic.tilt = tilt_percent;
167 notify(device_id);
168 return true;
169}
170
171bool DeviceRegistry::rollback_optimistic(const std::string &device_id, bool failed_stop) {
172 auto it = devices_.find(device_id);
173 if (it == devices_.end() || it->second.optimistic.empty())
174 return false;
175 OptimisticState &optimistic = it->second.optimistic;
176 if (failed_stop && optimistic.stop_replaced_movement) {
177 ESP_LOGD(TAG, "Device %s: optimistic rollback (stop failed, movement toward %.0f resumes)", device_id.c_str(),
178 optimistic.stop_replaced_target);
179 const float resumed_target = optimistic.stop_replaced_target;
180 optimistic.clear();
181 optimistic.target = resumed_target;
183 } else {
184 ESP_LOGD(TAG, "Device %s: optimistic rollback (command failed)", device_id.c_str());
185 optimistic.clear();
186 }
187 notify(device_id);
188 return true;
189}
190
191void DeviceRegistry::confirm_optimistic_stop(const std::string &device_id) {
192 auto it = devices_.find(device_id);
193 if (it != devices_.end())
194 it->second.optimistic.forget_replaced_movement();
195}
196
197std::vector<std::string> DeviceRegistry::all_linked_devices() const {
198 std::set<std::string> ids;
199 for (const auto &pair : linked_remotes_)
200 ids.insert(pair.second.begin(), pair.second.end());
201 for (const auto &pair : linked_remote_classes_)
202 ids.insert(pair.second.begin(), pair.second.end());
203 return {ids.begin(), ids.end()};
204}
205
207 const std::function<void(const std::string &, const std::vector<std::string> &)> &fn) const {
208 for (const auto &pair : linked_remotes_)
209 fn(pair.first, pair.second);
210}
211
212} // namespace home_io_control
213} // namespace esphome
const std::vector< std::string > * linked_devices(const std::string &remote_id) const
Retrieve the list of device IDs linked to a remote.
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.
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.
Per-hub device table, update-callback fan-out, and linked-remote map.
static constexpr const char * TAG
std::function< void(const std::string &device_id, const IoDevice &device)> DeviceUpdateCallback
Callback type invoked when a device's state changes.
void resolve_device_features(IoDevice &dev)
Resolve a device's capability bits and inversion from its kind and its YAML overrides.
DeviceType
IO-Homecontrol node types.
constexpr bool has_feature(DeviceFeatures set, DeviceFeatures bit)
True when every bit of bit is set in set.
TravelSpeed
The speed a cover's position moves ask the motor for (ADR 0065).
bool hex_to_bytes(const std::string &hex, uint8_t *out, uint8_t len)
Convert a hex string (e.g., "123ABC") to a byte array.
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
Fundamental IO-Homecontrol frame and crypto size constants.
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.
DeviceCapabilityClass entity_class
Class of the entity that declared the device; UNKNOWN for a device no entity declared (the type's cla...
std::optional< bool > invert_override
YAML invert_position: when declared; wins over the kind's default.
bool low_power
YAML-declared: this target is a low-power / duty-cycled receiver, so directed frames set CTRL1_LOW_PO...
uint8_t subtype
Device subtype (manufacturer‑specific).
bool battery_entities
YAML-declared (battery: true): the battery entities exist, so the status log line names the level eve...
DeviceModel model
YAML-declared product model below the subtype (io_model:); never on the wire.
uint8_t node_id[NODE_ID_SIZE]
Device's 3‑byte radio address.
bool dimmable
YAML-declared dimmable: true of a LIGHT entity.
uint8_t system_key[AES_KEY_SIZE]
The device's own key; meaningful only when has_system_key.
bool has_system_key
YAML-declared (system_key:): this device keeps a key of its own.
bool optimistic_state
True if target may be set ahead of a confirming poll/response.
DeviceType type
Device type (shutter, awning, etc.).
TravelSpeed travel_speed
Speed profile of position moves.
What the hub predicted ahead of confirmation, kept apart from what the device reported.
@ STOPPED
Predicted to be at rest (a STOP was issued).
@ MOVING
Predicted to be travelling (a position command was issued).
void clear()
Withdraw every prediction.
float stop_replaced_target
The replaced target; meaningful only with the flag.
Motion motion
Predicted movement state.
bool stop_replaced_movement
True while a pending STOP replaced a Motion::MOVING prediction that a failed STOP should bring back (...
void forget_replaced_movement()
Drop the movement a STOP replaced, once nothing can roll the STOP back any more.
float target
Predicted main-position target, or UNKNOWN_POSITION.