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 <string>
31#include <utility>
32
33namespace esphome {
34namespace home_io_control {
35
36/// @brief Mixin holding the hub-device binding shared by all IO-Homecontrol entity platforms.
37/// @ingroup hioc_platforms
39 public:
40 /// @brief Set the parent controller component.
41 /// @param parent Pointer to the IOHomeControlComponent instance.
42 void set_parent(IOHomeControlComponent *parent) { this->parent_ = parent; }
43 /// @brief Set the unique IO-homecontrol device ID (from YAML).
44 /// @param id Hexadecimal node ID string (e.g., "123ABC").
45 void set_device_id(const std::string &id) { this->device_id_ = id; }
46 /// @brief Set the declared device type (from YAML).
47 /// @param type Device type enum.
48 void set_device_type(DeviceType type) { this->device_type_ = type; }
49 /// @brief Set the declared device subtype (from YAML).
50 /// @param subtype Subtype value.
51 void set_subtype(uint8_t subtype) { this->subtype_ = subtype; }
52 /// @brief Enable or disable optimistic target updates for this device (from YAML).
53 /// Only covers expose this in YAML today; other platforms keep the default (true), which is
54 /// inert for entity types that never consult the effective target / movement state.
55 /// @param optimistic_state False to disable optimistic state; default true.
56 void set_optimistic_state(bool optimistic_state) { this->optimistic_state_ = optimistic_state; }
57 /// @brief Send this device's position moves in "silent operation" mode — the reference hub's
58 /// slower travel profile. Cover-only in practice; harmless on other platforms, which never
59 /// issue position moves.
60 void set_silent(bool silent) { this->silent_ = silent; }
61 /// @brief Mark this device as a low-power / duty-cycled receiver. Directed frames to it then set
62 /// CTRL1_LOW_POWER and use the long wake-up preamble; an always-alive device (the default) gets
63 /// neither. Radio property of the device, shared by all four platforms.
64 /// @param low_power True for a battery/solar or otherwise sleeping receiver; default false.
65 void set_low_power(bool low_power) { this->low_power_ = low_power; }
66 /// @brief Give this device a system key of its own (`system_key:` in YAML).
67 /// @details Every exchange with the device then uses this key instead of the hub's. The key is
68 /// validated as 32 hex characters at config time, so a parse failure here leaves the device on the
69 /// hub key.
70 /// @param key 32 hex characters.
71 void set_system_key(const std::string &key) {
72 this->has_system_key_ = hex_to_bytes(key, this->system_key_, AES_KEY_SIZE);
73 }
74 /// @brief Record that the YAML declared `battery: true`, so the status log line names the battery
75 /// level even when the device reports none. Cover-only in practice.
76 void set_battery_entities(bool battery_entities) { this->battery_entities_ = battery_entities; }
77 /// @brief Configure bounded follow-up polling while a state change is expected.
78 /// @param poll_interval_ms Poll interval in milliseconds; zero keeps the default single settle poll only.
79 void set_status_poll_interval(uint32_t poll_interval_ms) { this->status_poll_interval_ms_ = poll_interval_ms; }
80
81 protected:
82 /// @brief Perform the shared setup() registration ritual.
83 ///
84 /// Registers the device with the controller, sets its status-poll interval, subscribes the
85 /// entity's update callback, and schedules the delayed initial status request — in the exact
86 /// order every entity used before.
87 /// @param self The entity itself; used for set_timeout(), since DeviceBoundEntity is not a Component.
88 /// @param inverted Initial inversion flag passed to add_device (covers compute it; others pass false).
89 /// @param on_update The entity's device-update callback.
90 void register_device_binding_(Component *self, bool inverted,
91 std::function<void(const std::string &, const IoDevice &)> on_update) {
92 DeviceConfig config{this->device_type_, this->subtype_, inverted, this->optimistic_state_, this->silent_,
93 this->low_power_, this->battery_entities_};
94 config.has_system_key = this->has_system_key_;
95 if (this->has_system_key_)
96 memcpy(config.system_key, this->system_key_, AES_KEY_SIZE);
97 this->parent_->add_device(this->device_id_, config);
99 this->parent_->register_device_callback(std::move(on_update));
100 // Schedule the delayed initial poll through the public scheduler: Component::set_timeout is
101 // protected, and DeviceBoundEntity is a mixin, not a Component subclass, so it cannot call it
102 // through `self`. App.scheduler.set_timeout is the public entry point set_timeout wraps.
103 App.scheduler.set_timeout(self, "init_status", INITIAL_STATUS_REQUEST_DELAY_MS,
104 [this]() { this->parent_->queue_request_device_status(this->device_id_); });
105 }
106
107 /// @brief Shared inbound-update guard for binary endpoints.
108 ///
109 /// Matches this device and accepts only a settled (stopped) status with a known position.
110 /// Covers and locks intentionally use their own richer guards instead of this filter.
111 [[nodiscard]] bool passes_binary_update_filter_(const std::string &id, const IoDevice &dev) const {
112 return id == this->device_id_ && dev.position != UNKNOWN_POSITION && effective_is_stopped(dev);
113 }
114
115 /// @brief Emit the shared two-branch poll-interval line for dump_config().
116 /// @param tag Log tag of the calling entity.
117 void log_poll_interval_config_(const char *tag) const {
118 if (this->status_poll_interval_ms_ == 0) {
119 ESP_LOGCONFIG(tag, " Status Poll Interval: device-hinted settle polling (no fixed interval)");
120 } else {
121 ESP_LOGCONFIG(tag, " Status Poll Interval: %" PRIu32 " ms", this->status_poll_interval_ms_);
122 }
123 }
124
126 std::string device_id_;
128 uint8_t subtype_{0};
131 bool silent_{false};
132 bool low_power_{false};
133 bool battery_entities_{false};
134 bool has_system_key_{false};
135 uint8_t system_key_[AES_KEY_SIZE]{}; ///< Key material: never logged.
136};
137
138/// @brief Mixin holding the parent + device-id binding shared by per-device entities that are
139/// not full entity platforms: the auto-generated companion diagnostic sensors (device name,
140/// active issue, RSSI, last contact, exchange failures, last commanded by, last command source)
141/// and the per-device auxiliary control entities (cover favorite/vent buttons, cover silent
142/// switch).
143/// @ingroup hioc_platforms
144///
145/// These differ from the main entity platforms (DeviceBoundEntity above) in that they do not own
146/// the device: they never call add_device() or configure polling. The companion sensors use
147/// register_companion_binding_() to subscribe to update notifications and republish their one
148/// value; the auxiliary control entities take only the parent + device-id pair and act on their
149/// already-registered device on demand, so register_companion_binding_() stays opt-in. Each class
150/// keeps its own value rendering / action logic (including whether a given device state is
151/// publishable at all).
153 public:
154 /// @brief Set the parent controller component.
155 /// @param parent Pointer to the IOHomeControlComponent instance.
156 void set_parent(IOHomeControlComponent *parent) { this->parent_ = parent; }
157 /// @brief Set the device ID whose state this companion sensor exposes.
158 /// @param id Hexadecimal node ID string (for example "123ABC").
159 void set_device_id(const std::string &id) { this->device_id_ = id; }
160
161 protected:
162 /// @brief Shared setup() body: subscribe to this device's updates and publish the initial state.
163 ///
164 /// No-op when no parent is wired (codegen always wires one before setup() runs).
165 /// @param publish Renders and publishes the sensor's value from a device record — or skips
166 /// publishing when the record has no meaningful value yet. Runs once immediately when the
167 /// device is already registered, then again on every update notification for this device.
168 void register_companion_binding_(const std::function<void(const IoDevice &)> &publish) {
169 if (this->parent_ == nullptr)
170 return;
171
172 this->parent_->register_device_callback([this, publish](const std::string &id, const IoDevice &dev) {
173 if (id == this->device_id_)
174 publish(dev);
175 });
176
177 if (const auto *dev = this->parent_->get_device(this->device_id_); dev != nullptr)
178 publish(*dev);
179 }
180
182 std::string device_id_;
183};
184
185/// @brief Mixin for entities bound to the hub itself rather than to one device.
186/// @ingroup hioc_platforms
187///
188/// The hub-level control entities (discover button, scan-paired-devices button, arming switches,
189/// firmware-update button, 1W command/enroll buttons, pairing-result and 1W-last-command text
190/// sensors) need only a parent
191/// pointer — they act on the hub as a whole and have no device to bind to. This holds that one
192/// setter and one member so each entity does not restate it. The tuning number/select entities
193/// are deliberately not migrated: they take the parent by constructor injection and keep
194/// hub_core.h out of their header via a forward declaration, which this mixin's include of
195/// hub_core.h would defeat.
196///
197/// Why every one of these is created from the `home_io_control:` block (or a dedicated `button:`
198/// entry) and never from a device-bound `switch:`/`button:` platform entry: a user-declared
199/// device-bound entry would have to dispatch on the presence of a key (`io_device_id`,
200/// `commands:`, `enrollment:`, …) to decide what the entity *is*, so an entry that merely omitted
201/// `io_device_id` by mistake could be misread as one of these security-sensitive hub entities
202/// instead of failing validation. Creating them from the hub block removes that failure mode:
203/// there is no shared schema for a device-bound entity and a hub entity to be confused under.
204/// This is the shared reason behind all of them; each entity's own doc adds only what is
205/// specific to it (the ADR 0021 permission-vs-arming argument, the ADR 0026 physical-interlock
206/// argument, the "2W extraction and 1W adoption are deliberately independent" note, and so on).
208 public:
209 /// @brief Set the parent controller component.
210 /// @param parent Pointer to the IOHomeControlComponent instance.
211 void set_parent(IOHomeControlComponent *parent) { this->parent_ = parent; }
212
213 protected:
215};
216
217/// @brief Mixin for the hub-level entities that additionally scope themselves to one
218/// `oneway_controllers:` identity: the 1W command buttons, the 1W enrollment button, and the
219/// per-identity "Last 1W Command" text sensor.
220/// @ingroup hioc_platforms
221///
222/// They act on the hub, not on a paired device, so they are HubBoundEntity plus this one handle.
223/// The identity is a `oneway_controllers:` block entry, not an address in the hub's device table.
225 public:
226 /// @brief Set the controller identity this entity acts as / reports on.
227 /// @param id Handle from the `oneway_controllers:` block.
228 void set_controller_id(const std::string &id) { this->controller_id_ = id; }
229
230 protected:
231 std::string controller_id_;
232};
233
234} // namespace home_io_control
235} // 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 register_device_binding_(Component *self, bool inverted, std::function< void(const std::string &, const IoDevice &)> on_update)
Perform the shared setup() registration ritual.
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_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_silent(bool silent)
Send this device's position moves in "silent operation" mode — the reference hub's slower travel prof...
void set_device_type(DeviceType type)
Set the declared device type (from YAML).
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.
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:502
virtual IoDevice * get_device(const std::string &device_id)
Retrieve a device by ID; returns nullptr if not found.
Definition hub_core.cpp:350
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:344
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:320
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.
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.
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 has_system_key
system_key: declared: the device keeps a key of its own.
uint8_t system_key[AES_KEY_SIZE]
That key; meaningful only when has_system_key. Never logged.
Runtime state of a paired IO‑Homecontrol device.
float position
Current position: 0=open, 100=closed, or UNKNOWN_POSITION.