Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
platform_entity_base.h
Go to the documentation of this file.
1#pragma once
2
3/// @file platform_entity_base.h
4/// @brief Shared device-binding mixins for IO-Homecontrol entity platforms.
5/// @ingroup hioc_platforms
6///
7/// IOHomeCover, IOHomeLight, IOHomeSwitch and IOHomeLock all bind an ESPHome entity to a hub
8/// device: the same five YAML setters, the same registration ritual in setup(), and the same
9/// poll-interval dump_config line. DeviceBoundEntity centralizes exactly that shared state and
10/// wiring. The auto-generated companion diagnostic sensors share a smaller, observe-only
11/// binding; DeviceBoundCompanion centralizes that one. A third mixin, HubBoundEntity, holds the
12/// single parent pointer that the hub-level control entities (which act on the hub as a whole and
13/// have no device to bind to) would otherwise each restate.
14///
15/// All three are intentionally NOT ESPHome base classes — they are plain mixins the entities inherit
16/// alongside their real ESPHome base (cover::Cover, light::LightOutput, switch_::Switch,
17/// lock::Lock, text_sensor::TextSensor, sensor::Sensor). Entity-specific state mapping (cover
18/// position/tilt/movement inference, binary on/off decoding, each companion's value rendering)
19/// deliberately stays in the entities; only the device-binding plumbing lives here.
20
21#include "hub_core.h"
22#include "proto_frame.h"
23
24#include "esphome/core/application.h"
25#include "esphome/core/log.h"
26
27#include <cinttypes>
28#include <cstring>
29#include <functional>
30#include <optional>
31#include <string>
32#include <utility>
33
34namespace esphome {
35namespace home_io_control {
36
37/// @brief Mixin holding the hub-device binding shared by all IO-Homecontrol entity platforms.
38/// @ingroup hioc_platforms
40 public:
41 /// @brief Set the parent controller component.
42 /// @param parent Pointer to the IOHomeControlComponent instance.
43 void set_parent(IOHomeControlComponent *parent) { this->parent_ = parent; }
44 /// @brief Set the unique IO-homecontrol device ID (from YAML).
45 /// @param id Hexadecimal node ID string (e.g., "123ABC").
46 void set_device_id(const std::string &id) { this->device_id_ = id; }
47 /// @brief Set the declared device type (from YAML).
48 /// @param type Device type enum.
49 void set_device_type(DeviceType type) { this->device_type_ = type; }
50 /// @brief Set the declared device subtype (from YAML).
51 /// @param subtype Subtype value.
52 void set_subtype(uint8_t subtype) { this->subtype_ = subtype; }
53 /// @brief Set the declared product model below the subtype (YAML `io_model:`).
54 /// @param model Model; the codegen only emits a model the declared (type, subtype) offers.
55 void set_device_model(DeviceModel model) { this->model_ = model; }
56 /// @brief Enable or disable optimistic target updates for this device (from YAML).
57 /// Only covers expose this in YAML today; other platforms keep the default (true), which is
58 /// inert for entity types that never consult the effective target / movement state.
59 /// @param optimistic_state False to disable optimistic state; default true.
60 void set_optimistic_state(bool optimistic_state) { this->optimistic_state_ = optimistic_state; }
61 /// @brief Set the speed profile of this device's position moves (from YAML `travel_speed:` or
62 /// `silent:`). Cover-only in practice; harmless on other platforms, which never issue position moves.
63 /// @param speed Speed profile; FAST unless the cover declares otherwise.
64 void set_travel_speed(TravelSpeed speed) { this->travel_speed_ = speed; }
65 /// @brief Mark this device as a low-power / duty-cycled receiver. Directed frames to it then set
66 /// CTRL1_LOW_POWER and use the long wake-up preamble; an always-alive device (the default) gets
67 /// neither. Radio property of the device, shared by all four platforms.
68 /// @param low_power True for a battery/solar or otherwise sleeping receiver; default false.
69 void set_low_power(bool low_power) { this->low_power_ = low_power; }
70 /// @brief Give this device a system key of its own (`system_key:` in YAML).
71 /// @details Every exchange with the device then uses this key instead of the hub's. The key is
72 /// validated as 32 hex characters at config time, so a parse failure here leaves the device on the
73 /// hub key.
74 /// @param key 32 hex characters.
75 void set_system_key(const std::string &key) {
76 this->has_system_key_ = hex_to_bytes(key, this->system_key_, AES_KEY_SIZE);
77 }
78 /// @brief Record that the YAML declared `battery: true`, so the status log line names the battery
79 /// level even when the device reports none. Cover-only in practice.
80 void set_battery_entities(bool battery_entities) { this->battery_entities_ = battery_entities; }
81 /// @brief Configure bounded follow-up polling while a state change is expected.
82 /// @param poll_interval_ms Poll interval in milliseconds; zero keeps the default single settle poll only.
83 void set_status_poll_interval(uint32_t poll_interval_ms) { this->status_poll_interval_ms_ = poll_interval_ms; }
84
85 protected:
86 /// @brief Perform the shared setup() registration ritual.
87 ///
88 /// Registers the device with the controller, sets its status-poll interval, subscribes the
89 /// entity's update callback, and schedules the delayed initial status request — in the exact
90 /// order every entity used before.
91 /// @param self The entity itself; used for set_timeout(), since DeviceBoundEntity is not a Component.
92 /// @param entity_class Capability class of the registering entity; decides the features of a device
93 /// whose kind the type table does not know.
94 /// @param invert_override The YAML `invert_position:` when declared (covers only), else nullopt so the
95 /// kind's default applies.
96 /// @param on_update The entity's device-update callback.
97 /// @param dimmable The YAML `dimmable: true` of a light; false for every other entity.
98 void register_device_binding_(Component *self, DeviceCapabilityClass entity_class,
99 std::optional<bool> invert_override,
100 std::function<void(const std::string &, const IoDevice &)> on_update,
101 bool dimmable = false) {
102 DeviceConfig config;
103 config.type = this->device_type_;
104 config.subtype = this->subtype_;
105 config.model = this->model_;
106 config.entity_class = entity_class;
107 config.invert_override = invert_override;
108 config.dimmable = dimmable;
109 config.optimistic_state = this->optimistic_state_;
110 config.travel_speed = this->travel_speed_;
111 config.low_power = this->low_power_;
112 config.battery_entities = this->battery_entities_;
113 config.has_system_key = this->has_system_key_;
114 if (this->has_system_key_)
115 memcpy(config.system_key, this->system_key_, AES_KEY_SIZE);
116 this->parent_->add_device(this->device_id_, config);
118 this->parent_->register_device_callback(std::move(on_update));
119 // Schedule the delayed initial poll through the public scheduler: Component::set_timeout is
120 // protected, and DeviceBoundEntity is a mixin, not a Component subclass, so it cannot call it
121 // through `self`. App.scheduler.set_timeout is the public entry point set_timeout wraps.
122 App.scheduler.set_timeout(self, "init_status", INITIAL_STATUS_REQUEST_DELAY_MS,
123 [this]() { this->parent_->queue_request_device_status(this->device_id_); });
124 }
125
126 /// @brief What this entity's device can do: the registered device's features, or, before it is
127 /// registered, the features its declared kind resolves to.
128 /// @param entity_class Capability class of the asking entity.
129 [[nodiscard]] DeviceFeatures device_features_(DeviceCapabilityClass entity_class) const {
130 if (this->parent_ != nullptr) {
131 if (const IoDevice *dev = this->parent_->get_device(this->device_id_))
132 return dev->features;
133 }
134 IoDevice declared{};
135 declared.type = this->device_type_;
136 declared.subtype = this->subtype_;
137 declared.model = this->model_;
138 declared.entity_class = entity_class;
139 resolve_device_features(declared);
140 return declared.features;
141 }
142
143 /// @brief Shared inbound-update guard for binary endpoints.
144 ///
145 /// Matches this device and accepts only a settled (stopped) status with a known position.
146 /// Covers and locks intentionally use their own richer guards instead of this filter.
147 [[nodiscard]] bool passes_binary_update_filter_(const std::string &id, const IoDevice &dev) const {
148 return id == this->device_id_ && dev.position != UNKNOWN_POSITION && effective_is_stopped(dev);
149 }
150
151 /// @brief Emit the shared two-branch poll-interval line for dump_config().
152 /// @param tag Log tag of the calling entity.
153 void log_poll_interval_config_(const char *tag) const {
154 if (this->status_poll_interval_ms_ == 0) {
155 ESP_LOGCONFIG(tag, " Status Poll Interval: device-hinted settle polling (no fixed interval)");
156 } else {
157 ESP_LOGCONFIG(tag, " Status Poll Interval: %" PRIu32 " ms", this->status_poll_interval_ms_);
158 }
159 }
160
162 std::string device_id_;
164 uint8_t subtype_{0};
169 bool low_power_{false};
170 bool battery_entities_{false};
171 bool has_system_key_{false};
172 uint8_t system_key_[AES_KEY_SIZE]{}; ///< Key material: never logged.
173};
174
175/// @brief Mixin holding the parent + device-id binding shared by per-device entities that are
176/// not full entity platforms: the auto-generated companion diagnostic sensors (device name,
177/// active issue, RSSI, last contact, exchange failures, last commanded by, last command source)
178/// and the per-device auxiliary control entities (cover favorite/vent buttons, cover silent
179/// switch).
180/// @ingroup hioc_platforms
181///
182/// These differ from the main entity platforms (DeviceBoundEntity above) in that they do not own
183/// the device: they never call add_device() or configure polling. The companion sensors use
184/// register_companion_binding_() to subscribe to update notifications and republish their one
185/// value; the auxiliary control entities take only the parent + device-id pair and act on their
186/// already-registered device on demand, so register_companion_binding_() stays opt-in. Each class
187/// keeps its own value rendering / action logic (including whether a given device state is
188/// publishable at all).
190 public:
191 /// @brief Set the parent controller component.
192 /// @param parent Pointer to the IOHomeControlComponent instance.
193 void set_parent(IOHomeControlComponent *parent) { this->parent_ = parent; }
194 /// @brief Set the device ID whose state this companion sensor exposes.
195 /// @param id Hexadecimal node ID string (for example "123ABC").
196 void set_device_id(const std::string &id) { this->device_id_ = id; }
197
198 protected:
199 /// @brief Shared setup() body: subscribe to this device's updates and publish the initial state.
200 ///
201 /// No-op when no parent is wired (codegen always wires one before setup() runs).
202 /// @param publish Renders and publishes the sensor's value from a device record — or skips
203 /// publishing when the record has no meaningful value yet. Runs once immediately when the
204 /// device is already registered, then again on every update notification for this device.
205 void register_companion_binding_(const std::function<void(const IoDevice &)> &publish) {
206 if (this->parent_ == nullptr)
207 return;
208
209 this->parent_->register_device_callback([this, publish](const std::string &id, const IoDevice &dev) {
210 if (id == this->device_id_)
211 publish(dev);
212 });
213
214 if (const auto *dev = this->parent_->get_device(this->device_id_); dev != nullptr)
215 publish(*dev);
216 }
217
219 std::string device_id_;
220};
221
222/// @brief Mixin for entities bound to the hub itself rather than to one device.
223/// @ingroup hioc_platforms
224///
225/// The hub-level control entities (discover button, scan-paired-devices button, arming switches,
226/// firmware-update button, 1W command/enroll buttons, pairing-result and 1W-last-command text
227/// sensors) need only a parent
228/// pointer — they act on the hub as a whole and have no device to bind to. This holds that one
229/// setter and one member so each entity does not restate it. The tuning number/select entities
230/// are deliberately not migrated: they take the parent by constructor injection and keep
231/// hub_core.h out of their header via a forward declaration, which this mixin's include of
232/// hub_core.h would defeat.
233///
234/// Why every one of these is created from the `home_io_control:` block (or a dedicated `button:`
235/// entry) and never from a device-bound `switch:`/`button:` platform entry: a user-declared
236/// device-bound entry would have to dispatch on the presence of a key (`io_device_id`,
237/// `commands:`, `enrollment:`, …) to decide what the entity *is*, so an entry that merely omitted
238/// `io_device_id` by mistake could be misread as one of these security-sensitive hub entities
239/// instead of failing validation. Creating them from the hub block removes that failure mode:
240/// there is no shared schema for a device-bound entity and a hub entity to be confused under.
241/// This is the shared reason behind all of them; each entity's own doc adds only what is
242/// specific to it (the ADR 0021 permission-vs-arming argument, the ADR 0026 physical-interlock
243/// argument, the "2W extraction and 1W adoption are deliberately independent" note, and so on).
245 public:
246 /// @brief Set the parent controller component.
247 /// @param parent Pointer to the IOHomeControlComponent instance.
248 void set_parent(IOHomeControlComponent *parent) { this->parent_ = parent; }
249
250 protected:
252};
253
254/// @brief Mixin for the hub-level entities that additionally scope themselves to one
255/// `oneway_controllers:` identity: the 1W command buttons, the 1W enrollment button, and the
256/// per-identity "Last 1W Command" text sensor.
257/// @ingroup hioc_platforms
258///
259/// They act on the hub, not on a paired device, so they are HubBoundEntity plus this one handle.
260/// The identity is a `oneway_controllers:` block entry, not an address in the hub's device table.
262 public:
263 /// @brief Set the controller identity this entity acts as / reports on.
264 /// @param id Handle from the `oneway_controllers:` block.
265 void set_controller_id(const std::string &id) { this->controller_id_ = id; }
266
267 protected:
268 std::string controller_id_;
269};
270
271} // namespace home_io_control
272} // namespace esphome
Mixin holding the parent + device-id binding shared by per-device entities that are not full entity p...
void set_parent(IOHomeControlComponent *parent)
Set the parent controller component.
void register_companion_binding_(const std::function< void(const IoDevice &)> &publish)
Shared setup() body: subscribe to this device's updates and publish the initial state.
void set_device_id(const std::string &id)
Set the device ID whose state this companion sensor exposes.
Mixin holding the hub-device binding shared by all IO-Homecontrol entity platforms.
uint8_t system_key_[AES_KEY_SIZE]
Key material: never logged.
void set_battery_entities(bool battery_entities)
Record that the YAML declared battery: true, so the status log line names the battery level even when...
void set_travel_speed(TravelSpeed speed)
Set the speed profile of this device's position moves (from YAML travel_speed: or silent:).
void set_low_power(bool low_power)
Mark this device as a low-power / duty-cycled receiver.
void set_status_poll_interval(uint32_t poll_interval_ms)
Configure bounded follow-up polling while a state change is expected.
void set_optimistic_state(bool optimistic_state)
Enable or disable optimistic target updates for this device (from YAML).
void log_poll_interval_config_(const char *tag) const
Emit the shared two-branch poll-interval line for dump_config().
void set_device_model(DeviceModel model)
Set the declared product model below the subtype (YAML io_model:).
void set_device_type(DeviceType type)
Set the declared device type (from YAML).
void register_device_binding_(Component *self, DeviceCapabilityClass entity_class, std::optional< bool > invert_override, std::function< void(const std::string &, const IoDevice &)> on_update, bool dimmable=false)
Perform the shared setup() registration ritual.
void set_subtype(uint8_t subtype)
Set the declared device subtype (from YAML).
void set_device_id(const std::string &id)
Set the unique IO-homecontrol device ID (from YAML).
void set_system_key(const std::string &key)
Give this device a system key of its own (system_key: in YAML).
void set_parent(IOHomeControlComponent *parent)
Set the parent controller component.
bool passes_binary_update_filter_(const std::string &id, const IoDevice &dev) const
Shared inbound-update guard for binary endpoints.
DeviceFeatures device_features_(DeviceCapabilityClass entity_class) const
What this entity's device can do: the registered device's features, or, before it is registered,...
Mixin for entities bound to the hub itself rather than to one device.
void set_parent(IOHomeControlComponent *parent)
Set the parent controller component.
The main IO-Homecontrol component.
Definition hub_core.h:100
virtual void register_device_callback(DeviceUpdateCallback cb)
Register a callback invoked when any device updates.
Definition hub_core.h:521
virtual IoDevice * get_device(const std::string &device_id)
Retrieve a device by ID; returns nullptr if not found.
Definition hub_core.cpp:402
virtual void queue_request_device_status(const std::string &device_id)
Queue an async status request; returns immediately, executed in loop().
virtual void add_device(const std::string &device_id)
Add a device to the registry by device ID only (undeclared/legacy path).
Definition hub_core.cpp:396
virtual void set_device_status_poll_interval(const std::string &device_id, uint32_t poll_interval_ms)
Configure the optional follow-up polling interval for a registered device.
Definition hub_core.cpp:343
Mixin for the hub-level entities that additionally scope themselves to one oneway_controllers: identi...
void set_controller_id(const std::string &id)
Set the controller identity this entity acts as / reports on.
IO-Homecontrol ESPHome component — protocol controller.
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.
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.
@ UNKNOWN
Unknown/unspecified device.
bool effective_is_stopped(const IoDevice &dev)
Whether a consumer should treat the device as at rest, prediction first.
uint16_t DeviceFeatures
Capability bits of a device kind; see device_feature.
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.
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.
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...
DeviceFeatures features
What this device can do, resolved by resolve_device_features().
float position
Current position: 0=open, 100=closed, or UNKNOWN_POSITION.
uint8_t subtype
Device subtype (manufacturer‑specific).
DeviceModel model
YAML-declared product model below the subtype (io_model:); never on the wire.
DeviceType type
Device type (shutter, awning, etc.).