Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
hub_core.h
Go to the documentation of this file.
1#pragma once
2
3/// @file hub_core.h
4/// @brief IO-Homecontrol ESPHome component — protocol controller.
5/// @ingroup hioc_hub
6///
7/// This component manages the IO-Homecontrol 2W protocol: sending commands,
8/// receiving responses with automatic authentication, device discovery/pairing,
9/// and device state tracking. Radio hardware is delegated to a RadioDriver
10/// implementation (SX1276, SX1262, etc.).
11///
12/// SPI configuration: MSB first, CPOL=0, CPHA=0 (Mode 0), 8 MHz clock.
13/// The component inherits SPIDevice and implements SpiAccess to bridge
14/// the ESPHome SPI framework to the radio driver.
15///
16/// Architecture notes:
17/// - setup() initializes radio, waits for YAML-driven device registration, and enters RX mode.
18/// - loop() drains the OperationQueue collaborator (serializes all radio work).
19/// - All outbound commands go through send_and_receive_, a thin wrapper around
20/// ExchangeEngine which owns retry, timing, and challenge-response auth.
21/// - Inbound frames are processed in process_received_packet_ and may trigger
22/// inbound authentication (ExchangeEngine::authenticate_request) if the device proves itself.
23/// - DeviceRegistry and its callbacks provide fan-out to platform entities
24/// (covers/lights/switches/locks); StatusPollPolicy schedules follow-up polls;
25/// PairingEngine owns pairing; ManagementActions owns the rename/identify/force-open
26/// hub-level Home Assistant actions.
27
28#include "esphome/core/component.h"
29#include "esphome/core/hal.h"
30#include "esphome/components/api/custom_api_device.h"
31#include "esphome/components/spi/spi.h"
32#include "proto_codecs.h"
33#include "proto_frame.h"
34#include "proto_heating.h"
35#include "radio_interface.h"
36#include "tuning_config.h"
37#include "hub_exchange.h"
38#include "hub_decisions.h"
39#include "hub_pairing.h"
40#include "device_registry.h"
41#include "status_poll_policy.h"
42#include "operation_queue.h"
43#include "exchange_engine.h"
44#include "pairing_engine.h"
45#include "management_actions.h"
47#include "oneway_controller.h"
48#include "oneway_transmitter.h"
49#include "oneway_key_adoption.h"
50// lr1121_firmware_update_controller.h forward-declares FlashDecision / BootloaderUpgradePath /
51// Lr1121FirmwareUpdater so lr1121_firmware_decisions.h and radio_lr1121_firmware_updater.h are
52// NOT pulled into hub_core.h — that header weight stays in the collaborator's .cpp alone.
54#include <map>
55#include <vector>
56#include <functional>
57
58namespace esphome {
59namespace home_io_control {
60
61inline constexpr uint8_t DEFAULT_TX_POWER_DBM = 17; ///< Default TX power used unless YAML overrides it.
62inline constexpr uint8_t DEFAULT_PA_PIN_PA_BOOST = 0x80; ///< SX1276 PA_CONFIG selector for the PA_BOOST output path.
63inline constexpr uint8_t DEFAULT_TCXO_VOLTAGE_SETTING_1P8V = 0x03; ///< SX1262 DIO3 setting value for a 1.8 V TCXO.
64inline constexpr size_t POSITION_TEXT_BUFFER_SIZE = 16; ///< Buffer for formatted position strings such as "100%".
65
66#ifdef IOHOME_LR1121_FIRMWARE_UPDATE
67/// Max value representable by Component::warn_if_blocking_over_ (a centisecond uint8_t, ~2550 ms
68/// ceiling). Raised for a flash excursion so the log fills with progress output rather than
69/// component-blocking warnings — a flash still runs far longer than 2550 ms, so this reduces
70/// warning spam, it cannot eliminate it. Lives here rather than in the collaborator because
71/// warn_if_blocking_over_ is protected on ESPHome's Component and only the initializer-list lambda
72/// below legitimately writes it.
73inline constexpr uint8_t LR1121_FLASH_WARN_BLOCKING_MAX_CS = 255;
74#endif
75
76// ============================================================================
77// Main Component
78// ============================================================================
79
80/// The main IO-Homecontrol component. Manages the protocol layer and delegates
81/// radio operations to a RadioDriver instance.
82///
83/// Inherits SPIDevice so that ESPHome's Python codegen can configure SPI pins.
84/// Implements SpiAccess to provide the radio driver with SPI bus access.
85/// @ingroup hioc_hub
86class IOHomeControlComponent : public Component,
87 public api::CustomAPIDevice,
88 public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
89 spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_8MHZ>,
90 public SpiAccess {
91 public:
92 /// Initialize ExchangeEngine, PairingEngine, and ManagementActions with double-pointer/
93 /// reference indirection so that test assignments (`comp.radio_ = &mock`) propagate
94 /// through all collaborators without calling setup().
100 // Capturing `this` is safe here: the callback is only ever invoked from send_burst(),
101 // long after construction. It injects the *ability* to transmit rather than a reference
102 // to whichever collaborator currently owns the radio.
103 oneway_transmitter_([this](const IoFrame &frame, uint32_t freq, uint16_t preamble) {
104 return this->transmit_frame_(frame, freq, preamble);
105 }),
106 // set_timeout() is protected on the real ESPHome Component (only public in the host stub),
107 // so a lambda defined here — with protected access — is the one legitimate caller. See
108 // oneway_key_adoption.h's NamedTimeoutFn.
109 oneway_key_adoption_([this](const char *name, uint32_t delay_ms, std::function<void()> cb) {
110 this->set_timeout(name, delay_ms, std::move(cb));
111 }),
114 [this](const IoFrame &frame, uint32_t freq, uint16_t preamble) {
115 return this->transmit_frame_(frame, freq, preamble);
116 },
117 [this](const char *name, uint32_t delay_ms, std::function<void()> cb) {
118 this->set_timeout(name, delay_ms, std::move(cb));
119 })
120#ifdef IOHOME_LR1121_FIRMWARE_UPDATE
121 // Guarded initializer for the guarded member. `busy_`/`warn_if_blocking_over_` are
122 // protected: the busy pointer is taken here, and the lambda — with protected access — is
123 // the one legitimate writer of warn_if_blocking_over_. `this` doubles as SpiAccess* and as
124 // the self key for App.scheduler.set_timeout(), never to reach a protected member.
125 ,
126 lr1121_firmware_update_(
127 &radio_, this, &rst_pin_, &busy_pin_, &busy_,
128 [this]() { this->warn_if_blocking_over_ = LR1121_FLASH_WARN_BLOCKING_MAX_CS; }, this)
129#endif
130 {
131 }
132
133 /// @brief Result payload used by hub-level management actions such as rename.
134 /// Alias of the standalone esphome::home_io_control::ManagementActionResult struct so that
135 /// callers using the nested name IOHomeControlComponent::ManagementActionResult continue to work.
137
138 /// @brief Initialize hardware (radio and device registry).
139 void setup() override;
140 /// @brief Main loop: process pending operations and drive radio state machine.
141 void loop() override;
142 /// @brief Dump configuration and radio debug info to the log.
143 void dump_config() override;
144 /// @brief Get setup priority (HARDWARE to initialize early).
145 /// @return setup_priority::HARDWARE.
146 [[nodiscard]] float get_setup_priority() const override { return setup_priority::HARDWARE; }
147
148 // --- SpiAccess implementation (delegates to SPIDevice) ---
149 /// @brief Enable the SPI bus.
150 void spi_enable() override { this->enable(); }
151 /// @brief Disable the SPI bus.
152 void spi_disable() override { this->disable(); }
153 /// @brief Transfer one byte full‑duplex.
154 /// @param data Byte to send.
155 /// @return Received byte.
156 uint8_t spi_transfer(uint8_t data) override { return this->transfer_byte(data); }
157 /// @brief Write one byte (MOSI only).
158 /// @param data Byte to send.
159 void spi_write(uint8_t data) override { this->write_byte(data); }
160 /// @brief Read one byte (MISO only).
161 /// @return Received byte.
162 uint8_t spi_read() override { return this->read_byte(); }
163
164 /// @brief Suspend the hub's normal loop (packet processing, hopping, polling).
165 /// Used by loopback test configs to take exclusive control of the radio.
166 void set_radio_test_mode(bool active) { this->radio_test_mode_ = active; }
167
168 /// @brief Get the underlying radio driver (for diagnostics and test tooling).
169 [[nodiscard]] RadioDriver *get_radio() const { return this->radio_; }
170
171 // --- YAML configuration setters (called by generated code) ---
172 /// Set the radio reset pin.
173 void set_rst_pin(InternalGPIOPin *pin) { this->rst_pin_ = pin; }
174 /// Set the DIO0 interrupt pin (SX1276).
175 void set_dio0_pin(InternalGPIOPin *pin) { this->dio0_pin_ = pin; }
176 /// Set the DIO4 preamble‑detect pin (SX1276, optional).
177 void set_dio4_pin(InternalGPIOPin *pin) { this->dio4_pin_ = pin; }
178 /// Set the DIO1 interrupt pin (SX1262; also carries the LR1121's DIO9 IRQ line).
179 void set_dio1_pin(InternalGPIOPin *pin) { this->dio1_pin_ = pin; }
180 /// Set the BUSY pin (SX1262/LR1121).
181 void set_busy_pin(InternalGPIOPin *pin) { this->busy_pin_ = pin; }
182 /// Set the front‑end module enable pin.
183 void set_fem_en_pin(InternalGPIOPin *pin) { this->fem_en_pin_ = pin; }
184 /// Set the VFEM power pin.
185 void set_vfem_pin(InternalGPIOPin *pin) { this->vfem_pin_ = pin; }
186 /// Set the FEM PA switch pin.
187 void set_fem_pa_pin(InternalGPIOPin *pin) { this->fem_pa_pin_ = pin; }
188 /// Set the controller's node ID (hex string).
189 void set_node_id(const std::string &id) { this->node_id_str_ = id; }
190 /// Set the system key (hex string).
191 void set_system_key(const std::string &key) { this->system_key_str_ = key; }
192 /// Set transmit power (dBm).
193 void set_tx_power(uint8_t power) { this->tx_power_ = power; }
194 /// Set PA boost pin configuration.
195 void set_pa_pin(uint8_t pa_pin) { this->pa_pin_ = pa_pin; }
196 /// Set radio type ("sx1276", "sx1262", or "lr1121"); required by the YAML schema.
197 void set_radio_type(const std::string &type) { this->radio_type_ = type; }
198 /// Set TCXO voltage for SX1262/LR1121 (1.8V / 3.3V).
199 void set_tcxo_voltage(uint8_t voltage) { this->tcxo_voltage_ = voltage; }
200
201 /// Apply the tuning configuration generated from YAML / UI entities.
202 void set_tuning_config(const TuningConfig &config) { this->tuning_ = config; }
203 /// Receive a numeric tuning update from a HA `number` entity.
204 void update_tuning_number(const std::string &name, float value);
205 /// Receive a select tuning update from a HA `select` entity.
206 void update_tuning_select(const std::string &name, const std::string &value);
207 /// Current value of a numeric tuning parameter, used to seed a HA `number` entity on boot.
208 /// @param name YAML key of the parameter.
209 /// @return Current value, or 0 for an unknown key.
210 [[nodiscard]] float get_tuning_number_value(const std::string &name) const;
211 /// Current option string of a select tuning parameter, used to seed a HA `select` entity on boot.
212 /// @param name YAML key of the parameter.
213 /// @return Current value formatted as its YAML option string, or empty for an unknown key.
214 [[nodiscard]] std::string get_tuning_select_value(const std::string &name) const;
215
216 /// @brief Render a device's "last commanded by" string, resolving this hub's own node ID.
217 ///
218 /// Thin wrapper over detail::describe_last_commander(); exists because the hub's node ID is not
219 /// reachable from a companion entity and hub_internal.h cannot be included from this header.
220 /// @param dev Device record to read.
221 /// @return See detail::describe_last_commander().
222 [[nodiscard]] std::string describe_last_commander(const IoDevice &dev) const;
223
224 /// Declare that a remote (identified by its node ID) controls a registered device.
225 /// When activity from this remote is overheard, a status poll is scheduled for the device.
226 /// This is needed for 1W remotes whose destination address differs from the device's 2W ID.
227 /// @param remote_id Node ID of the remote control.
228 /// @param device_id Node ID of the device it controls.
229 void add_linked_remote(const std::string &remote_id, const std::string &device_id) {
230 this->registry_.add_linked_remote(remote_id, device_id);
231 }
232
233 /// Declare that a device class's typed 1W broadcasts (e.g. "all awnings") also apply to
234 /// @p device_id, matching how 1W remotes address a device class rather than a single node.
235 /// @param type Device class the broadcast targets.
236 /// @param device_id Node ID of the device to add to that class.
237 void add_linked_remote_class(DeviceType type, const std::string &device_id) {
238 this->registry_.add_linked_remote_class(type, device_id);
239 }
240
241 /// Set an optimistic target position ahead of a confirming poll/response, and notify.
242 /// No-op when the device is unknown or has `optimistic_state == false`. See
243 /// DeviceRegistry::apply_optimistic_target() for the full contract.
244 /// Virtual (like add_device/get_device) so platform unit tests can substitute a mock registry.
245 /// @param device_id Target device ID.
246 /// @param target_io_position Target position in IO units (0=open, 100=closed).
247 /// @return true if the optimistic state was applied.
248 virtual bool apply_optimistic_target(const std::string &device_id, float target_io_position) {
249 return this->registry_.apply_optimistic_target(device_id, target_io_position);
250 }
251
252 /// Predict that a device has stopped (e.g. on STOP), and notify.
253 /// No-op when the device is unknown or has `optimistic_state == false`. See
254 /// DeviceRegistry::apply_optimistic_stop() for why this records a prediction rather than a clear.
255 /// Virtual (like add_device/get_device) so platform unit tests can substitute a mock registry.
256 /// @param device_id Target device ID.
257 /// @return true if the optimistic stop was applied.
258 virtual bool apply_optimistic_stop(const std::string &device_id) {
259 return this->registry_.apply_optimistic_stop(device_id);
260 }
261
262 /// Set an optimistic slat angle ahead of a confirming status poll, and notify.
263 /// No-op when the device is unknown, has `optimistic_state == false`, or is not tilt-capable.
264 /// See DeviceRegistry::apply_optimistic_tilt() for the full contract and for why a tilt
265 /// command cannot rely on its own reply the way a position command can.
266 /// Virtual like the other device-registry accessors so a test double can override it if it
267 /// needs to; MockPlatformHubBase deliberately does not, and exercises the real registry.
268 /// @param device_id Target device ID.
269 /// @param tilt_percent Slat angle in the same percent scale as `IoDevice::tilt` (0-100).
270 /// @return true if the optimistic tilt was applied.
271 virtual bool apply_optimistic_tilt(const std::string &device_id, float tilt_percent) {
272 return this->registry_.apply_optimistic_tilt(device_id, tilt_percent);
273 }
274
275 /// Allow a 1W sender (identified by its node ID) to fire the `esphome.home_io_control_sender_event`
276 /// event to Home Assistant. "Sender" is deliberately broader than "remote": the same 1W broadcast
277 /// mechanism carries handheld/wall remotes and wind/rain sensors alike (they differ only in the
278 /// `originator` byte inside the payload, not in how they address the radio) — see `decode_1w_frame()`.
279 /// Overheard 1W traffic is always DEBUG-logged regardless of this list; this only controls which
280 /// senders are allowed to reach Home Assistant as an event, independent of whether the sender is
281 /// also linked to a device via `add_linked_remote`. Empty by default — a sender must be explicitly
282 /// opted in.
283 /// @param sender_id Node ID of the 1W sender (remote or sensor).
284 void add_exposed_sender(const std::string &sender_id) { this->exposed_senders_.push_back(sender_id); }
285
286 /// Register a configured 1W controller identity (see oneway_controller.h). Called once per
287 /// `oneway_controllers:` entry from generated code. Both the source address and the key are
288 /// already resolved at schema time — a derived address is computed there so a collision with
289 /// the hub's own address or another identity fails the build rather than silently desyncing a
290 /// transmitter at runtime.
291 /// @param identity Fully-resolved controller identity.
293 this->oneway_transmitter_.add_identity(identity);
294 }
295
296 /// @return The configured 1W controller identities.
297 [[nodiscard]] const OneWayControllerRegistry &oneway_controllers() const {
298 return this->oneway_transmitter_.identities();
299 }
300
301 /// @brief Queue a 1W named command, sent as the given controller identity.
302 ///
303 /// Goes through the operation queue like every other radio operation (ADR 0013), so a 1W burst
304 /// can never interleave with a 2W exchange. Unlike a 2W command this reports nothing back: 1W
305 /// has no reply, so a queued command that a device ignores is indistinguishable from one it
306 /// obeyed. The "Last 1W Command" diagnostic reports what was *transmitted*, which is the only
307 /// half of that the hub can know.
308 /// @param controller_id Controller-identity handle from `oneway_controllers:`.
309 /// @param cmd Named command (STOP, FAVORITE, VENT, FORCE_OPEN).
310 void send_oneway_command(const std::string &controller_id, CoverCommand cmd) {
311 this->op_queue_.enqueue_oneway_command(controller_id, cmd);
312 }
313
314 /// @brief Queue a 1W numeric position, sent as the given controller identity.
315 /// @param controller_id Controller-identity handle from `oneway_controllers:`.
316 /// @param position Target position 0–100 (0 = fully open, 100 = fully closed).
317 void send_oneway_position(const std::string &controller_id, uint8_t position) {
318 this->op_queue_.enqueue_oneway_position(controller_id, position);
319 }
320
321 /// @brief Queue whichever of position/command a generated button's action resolves to.
322 ///
323 /// The mapping is applied here, at enqueue time, so the queue only ever holds concrete
324 /// operations — OPEN and CLOSE are positions on the wire, not commands, and nothing downstream
325 /// should have to know that twice.
326 /// @param controller_id Controller-identity handle from `oneway_controllers:`.
327 /// @param action Button action to send.
328 void send_oneway_action(const std::string &controller_id, OneWayButtonAction action) {
329 const OneWayActionEncoding encoding = encode_oneway_action(action);
330 if (encoding.is_position) {
331 this->send_oneway_position(controller_id, encoding.position);
332 } else {
333 this->send_oneway_command(controller_id, encoding.command);
334 }
335 }
336
337 /// @brief Queue a 1W enrollment for the given controller identity — the enroll button's press
338 /// handler.
339 ///
340 /// Sends `0x39` (self-directed) then `0x30`, back to back — see
341 /// OneWayTransmitter::send_enrollment().
342 /// @param controller_id Controller-identity handle from `oneway_controllers:`.
343 void send_oneway_enroll(const std::string &controller_id) { this->op_queue_.enqueue_oneway_enroll(controller_id); }
344
345 /// @brief Queue a standalone 1W un-enrollment (remove-controller) for the given controller
346 /// identity, reached only through the explicitly-named `oneway_remove_controller` native API
347 /// action — the same `0x39` send_oneway_enroll() also fires as its own prelude, but here alone.
348 ///
349 /// @warning **Unconfirmed standalone on real hardware.** Firing `0x39` alone has had no
350 /// observable effect on this project's test hardware; the leading hypothesis is that it needs
351 /// the same association-mode window enrollment does. See ADR 0026 § Consequences.
352 /// @param controller_id Controller-identity handle from `oneway_controllers:`.
353 void send_oneway_unenroll(const std::string &controller_id) {
354 this->op_queue_.enqueue_oneway_unenroll(controller_id);
355 }
356
357 /// @brief Subscribe to the report emitted after every 1W command attempt.
358 ///
359 /// A list rather than a single slot: each identity gets its own "Last 1W Command" sensor, and
360 /// each filters the reports down to its own handle.
361 /// @param callback Invoked for every attempt, successful or not.
363 this->oneway_report_callbacks_.push_back(std::move(callback));
364 }
365
366 /// @return The 1W transmit collaborator, for diagnostics and the sequence-resync path.
368
369 /// @return The telemetry recorded for the most recent (or in-progress) pairing attempt.
370 [[nodiscard]] const PairingTelemetry &pairing_telemetry() const { return this->pairing_telemetry_; }
371
372 /// Register a callback invoked once, right after every `discover_and_pair()` attempt
373 /// completes — used by the "Last Pairing Result" text sensor to publish a fresh value.
374 /// Single-slot: only one platform instance is expected per hub.
375 /// @param cb Callable with no arguments.
376 void set_pairing_result_callback(std::function<void()> cb) { this->pairing_result_callback_ = std::move(cb); }
377
378 /// @brief Arm or disarm the "Recover System Key" (key extraction) responder.
379 ///
380 /// Thin forwarder to the KeyExtractionResponder collaborator (key_extraction_responder.h).
381 /// Arming picks a fresh throwaway node ID, resets the pairing_responder state machine to
382 /// ARMED_IDLE, and schedules a 10-minute auto-off. While armed, the 0x28/0x2C/0x31/0x32 branches
383 /// in process_received_packet_() emulate an unpaired device so a user's existing hub can pair to
384 /// it and hand over its node_id/system_key (see pairing_responder.h). Disarming — manual, via
385 /// the HA switch, on successful extraction, or on auto-off — immediately stops those branches
386 /// from responding; it never touches the real device registry or the hub's own node_id_/
387 /// system_key_. Virtual so platform unit tests can substitute a mock hub, matching every other
388 /// queue_*/set_* entry point on this component.
389 /// @param armed Desired state.
390 virtual void set_key_extraction_armed(bool armed) { this->key_extraction_.set_armed(armed); }
391
392 /// Register a callback invoked whenever the key-extraction armed state changes — manual
393 /// toggle, successful extraction, or auto-off timeout — so the switch entity can keep its
394 /// displayed state in sync when the hub disarms itself rather than the user. Single-slot,
395 /// mirrors set_pairing_result_callback().
396 /// @param cb Callable receiving the new armed state.
397 void set_key_extraction_armed_callback(std::function<void(bool)> cb) {
398 this->key_extraction_.set_armed_callback(std::move(cb));
399 }
400
401 /// Arm or disarm the 1W controller-key adoption listener. Thin forwarder to the OnewayKeyAdoption
402 /// collaborator (oneway_key_adoption.h) — while armed, an overheard CMD_ONEWAY_ADD_CONTROLLER
403 /// broadcast is decrypted and reported once, after which the listener disarms itself (one
404 /// adoption per arm). Receive-only: unlike 2W key extraction this never transmits, it only
405 /// listens for a frame a 1W device broadcasts of its own accord. Virtual so platform unit tests
406 /// can substitute a mock hub, matching every other queue_*/set_* entry point on this component.
407 /// @param armed Desired state.
408 virtual void set_oneway_key_adoption_armed(bool armed) { this->oneway_key_adoption_.set_armed(armed); }
409
410 /// Register a callback invoked whenever the 1W key-adoption armed state changes — manual
411 /// toggle, successful adoption, or auto-off timeout — so the switch entity stays in sync when
412 /// the hub disarms itself rather than the user. Single-slot, mirrors
413 /// set_key_extraction_armed_callback().
414 /// @param cb Callable receiving the new armed state.
415 void set_oneway_key_adoption_armed_callback(std::function<void(bool)> cb) {
416 this->oneway_key_adoption_.set_armed_callback(std::move(cb));
417 }
418
419 /// Whether the 1W key-adoption listener is currently armed.
420 /// @return true while armed.
421 [[nodiscard]] bool oneway_key_adoption_armed() const { return this->oneway_key_adoption_.armed(); }
422
423 /// @brief Set whether ManagementActions::probe_device()/probe_sweep() are allowed to run.
424 ///
425 /// Set once from the `diagnostic_probes:` YAML boolean (`__init__.py`); off by default, so a
426 /// build that doesn't opt in never sends an undecoded probe opcode. Not a runtime toggle: there
427 /// is no entity and nothing else calls this after setup — the gate is "was this build
428 /// configured with `diagnostic_probes: true`", not a state a user flips per session.
429 /// @param enabled Desired state.
430 void set_diagnostic_probes_enabled(bool enabled) { this->diagnostic_probes_enabled_ = enabled; }
431
432 /// @brief Whether diagnostic probes are enabled for this build.
433 [[nodiscard]] bool diagnostic_probes_enabled() const { return this->diagnostic_probes_enabled_; }
434
435 // --- Device management (called by platform entities during setup) ---
436 /// Add a device to the registry by device ID only (undeclared/legacy path).
437 /// Type, subtype, inverted, and optimistic_state default to UNKNOWN / 0 / false / true; use the
438 /// `DeviceConfig` overload when metadata comes from a YAML declaration.
439 /// @param device_id Hexadecimal node ID string.
440 virtual void add_device(const std::string &device_id);
441 /// Add a device to the registry with full metadata from a YAML declaration.
442 /// @param device_id Hexadecimal node ID string.
443 /// @param cfg Device type/subtype/inversion/optimistic-state metadata.
444 virtual void add_device(const std::string &device_id, const DeviceConfig &cfg);
445 /// Retrieve a device by ID; returns nullptr if not found.
446 /// @param device_id Hexadecimal node ID.
447 /// @return Pointer to IoDevice, or nullptr.
448 virtual IoDevice *get_device(const std::string &device_id);
449 /// Set a device's `dimmable` flag (see IoDevice::dimmable). Called by platform_light.cpp's
450 /// setup(), not folded into add_device() since it's a light-only YAML choice. No-op if the
451 /// device isn't registered.
452 /// @param device_id Hexadecimal node ID string.
453 /// @param dimmable New value for IoDevice::dimmable.
454 virtual void set_device_dimmable(const std::string &device_id, bool dimmable);
455
456 /// Select a device's travel profile at runtime (see IOHomeCoverSilentSwitch).
457 /// Virtual for the same reason as set_device_dimmable: platform tests substitute a mock registry.
458 /// @param device_id Hexadecimal node ID string.
459 /// @param silent True to send position moves in "silent operation" (slower) mode.
460 virtual void set_device_silent(const std::string &device_id, bool silent);
461 /// Register a callback invoked when any device updates.
462 /// @param cb Callable with signature void(const std::string&, const IoDevice&).
463 virtual void register_device_callback(DeviceUpdateCallback cb) { this->registry_.subscribe(std::move(cb)); }
464 /// Configure the optional follow-up polling interval for a registered device.
465 /// @param device_id Target device ID.
466 /// @param poll_interval_ms Poll interval in milliseconds; zero keeps the legacy one-shot settle poll only.
467 virtual void set_device_status_poll_interval(const std::string &device_id, uint32_t poll_interval_ms);
468
469 // --- High-level operations ---
470 /// Send a position command to a device.
471 /// @param device_id Target device ID.
472 /// @param position Desired position, 0–100 (open→closed). Named commands (STOP, FAVORITE,
473 /// VENT) go through execute_device_command_()/create_execute_command() instead.
474 /// @return true if device acknowledged; false on timeout or radio error.
475 virtual bool set_device_position(const std::string &device_id, uint8_t position);
476 /// Send a tilt command to a tilt‑capable cover.
477 /// @param device_id Target device ID.
478 /// @param tilt_percent Desired tilt (0–100).
479 /// @return true if device acknowledged; false otherwise.
480 virtual bool set_device_tilt(const std::string &device_id, uint8_t tilt_percent);
481 /// Set both position and tilt of a tilt-capable cover in one atomic command.
482 /// @param device_id Target device ID.
483 /// @param position Desired position (0–100, open→closed).
484 /// @param tilt_percent Desired tilt (0–100).
485 /// @return true if device acknowledged; false otherwise.
486 virtual bool set_device_position_and_tilt(const std::string &device_id, uint8_t position, uint8_t tilt_percent);
487 /// Request current status from a device.
488 /// @param device_id Target device ID.
489 /// @return true if status frame was received and processed.
490 virtual bool request_device_status(const std::string &device_id);
491 /// Request the stored device name from a device.
492 /// @param device_id Target device ID.
493 /// @return true if a name response frame was received and processed.
494 virtual bool request_device_name(const std::string &device_id);
495 /// Rename a device and verify the result by reading the name back.
496 /// @param device_id Target device ID.
497 /// @param new_name Requested UTF-8 device name.
498 /// @return Structured result describing success, verification, and any validation failure.
499 virtual ManagementActionResult rename_device(const std::string &device_id, const std::string &new_name) {
500 return this->management_actions_.rename_device(device_id, new_name);
501 }
502 /// Trigger a device's physical identify (brief jog/flash) so a user can confirm which
503 /// physical motor a device ID maps to.
504 /// @param device_id Target device ID.
505 /// @return Structured result describing success and any validation failure. `verified` is
506 /// always false — there is no readback for a physical identify jog.
507 virtual ManagementActionResult identify_device(const std::string &device_id) {
508 return this->management_actions_.identify_device(device_id);
509 }
510 /// @brief Move a cover device to fully open at elevated priority, intended to bypass
511 /// wind/rain soft locks.
512 ///
513 /// Safety-sensitive: queues CoverCommand::FORCE_OPEN through the normal cover-command dispatch
514 /// path. Only confirms the command was queued; the movement outcome arrives later via the
515 /// device's normal cover-state/polling pipeline, so `verified` is always false. The lock-bypass
516 /// behavior itself is experimental and unconfirmed against an active lock — see
517 /// ManagementActions::force_open_device()'s doxygen for details.
518 /// @param device_id Target device ID.
519 /// @return Structured result describing whether the command was queued.
520 virtual ManagementActionResult force_open_device(const std::string &device_id) {
521 return this->management_actions_.force_open_device(device_id);
522 }
523 /// Broadcast a roll-call and report every device that answers (see
524 /// ManagementActions::scan_paired_devices() for the full contract: only key-holding devices
525 /// answer, DeviceRegistry is never written, and zero replies is a successful result).
526 /// @return Structured result whose `message` is the full multi-line report.
527 virtual ManagementActionResult scan_paired_devices() { return this->management_actions_.scan_paired_devices(); }
528 /// Send a single diagnostic probe frame to a registered device and report the raw reply (see
529 /// ManagementActions::probe_device() for the full contract, argument formats, and safety
530 /// gating). Protocol-research instrumentation for opcodes this codebase has not decoded — see
531 /// docs/radio_diagnostics.md and ADR 0024.
532 /// @param device_id Target device ID.
533 /// @param probe Probe name ("private_fn", "status_ext", "general_info3", "private2", or
534 /// "private2_short").
535 /// @param index Function ID / selector block / modifier, as a decimal or `0x`-prefixed hex
536 /// string; ignored for "general_info3".
537 /// @return Structured result whose `message` carries the reply's command byte and raw hex.
538 virtual ManagementActionResult probe_device(const std::string &device_id, const std::string &probe,
539 const std::string &index) {
540 return this->management_actions_.probe_device(device_id, probe, index);
541 }
542 /// Walk a bounded index range, one probe_device() call per index (see
543 /// ManagementActions::probe_sweep()).
544 /// @param device_id Target device ID.
545 /// @param probe Probe name, same as probe_device().
546 /// @param first_index First index in the sweep (inclusive).
547 /// @param last_index Last index in the sweep (inclusive).
548 /// @return Structured result whose `message` is one line per index.
549 virtual ManagementActionResult probe_sweep(const std::string &device_id, const std::string &probe,
550 const std::string &first_index, const std::string &last_index) {
551 return this->management_actions_.probe_sweep(device_id, probe, first_index, last_index);
552 }
553 /// @brief Run one 2W heating/climate function (CMD_WRITE_PRIVATE 0x20) against a registered
554 /// climate device — the `heating_control` hub action.
555 ///
556 /// Experimental: the protocol is derived from the iohomecontrol project's Cozytouch support and
557 /// has never been validated on real Atlantic/Thermor/Sauter hardware. `verified` is always
558 /// false: the `set_*` functions are write-only — nothing decodes what the radiator did into an
559 /// entity. (`power_on` and `midnight_sync` are register reads; their ACK payload is logged at
560 /// DEBUG but not decoded.) See ManagementActions::heating_control() for argument formats.
561 /// @param device_id Target device ID.
562 /// @param function Heating function name.
563 /// @param value Function-specific value string.
564 /// @return Structured result describing success and any validation/exchange failure.
565 virtual ManagementActionResult heating_control(const std::string &device_id, const std::string &function,
566 const std::string &value) {
567 return this->management_actions_.heating_control(device_id, function, value);
568 }
569 /// Discover and pair a device that is in pairing mode.
570 /// @return true if pairing completed successfully; false otherwise.
571 virtual bool discover_and_pair();
572 /// Send an arbitrary IO position (0-100) to a light entity. Internally mapped to the shared
573 /// execute path. set_light_state() is a thin binary-position wrapper around this, used by
574 /// dimmable lights to send anything other than the two binary extremes.
575 /// @param device_id Target device ID.
576 /// @param position Desired IO position (0-100); this device family's convention maps 0 to full
577 /// brightness and 100 to off, the same 0-100 scale platform_cover.cpp uses.
578 /// @return true if device acknowledged.
579 virtual bool set_light_position(const std::string &device_id, uint8_t position);
580 /// Semantic binary helper for light entities. Internally mapped to the shared execute path.
581 /// @param device_id Target device ID.
582 /// @param on Desired on/off state.
583 /// @return true if device acknowledged.
584 virtual bool set_light_state(const std::string &device_id, bool on);
585 /// Semantic binary helper for switch entities. Internally mapped to the shared execute path.
586 /// @param device_id Target device ID.
587 /// @param on Desired on/off state.
588 /// @return true if device acknowledged.
589 virtual bool set_switch_state(const std::string &device_id, bool on);
590 /// Semantic lock helper for lock entities. Internally mapped to the shared execute path.
591 /// @param device_id Target device ID.
592 /// @param locked Desired locked/unlocked state.
593 /// @return true if device acknowledged.
594 virtual bool set_lock_state(const std::string &device_id, bool locked);
595 /// @brief The single hub-side transmit path for 2W heating/climate control (CMD_WRITE_PRIVATE
596 /// 0x20). Both the `heating_control` hub action and the climate entity call this — there is
597 /// exactly one place that transmits heating frames and exactly one caller of
598 /// create_write_private().
599 ///
600 /// Flow: registry lookup -> device_supports_climate_control() gate (rejected via
601 /// detail::log_rejected_operation()) -> encode_heating_payload() -> create_write_private() (with
602 /// the device's `low_power` flag) -> a plain send_and_receive_(). Deliberately NOT routed
603 /// through execute_request_and_update_(): that helper is cover/position-shaped (status decode,
604 /// poll backoff), and a heater has no position and no status poll. A CMD_WRITE_PRIVATE_ACK
605 /// (0x21) reply is success; anything else (including CMD_ERROR_RESP) is failure. The exchange
606 /// still feeds the device-agnostic Last Contact / Exchange Failures link-health sensors.
607 ///
608 /// Write-only semantics: this only reports whether the device acknowledged the write. The `set_*`
609 /// functions decode nothing back into an entity, so callers publish "last commanded, never
610 /// confirmed" state on success and never at request time. `power_on` and `midnight_sync` are
611 /// register reads whose 0x21 ACK payload is logged at DEBUG (see the .cpp) but not decoded.
612 /// @param device_id Target device ID (hex string).
613 /// @param fn Heating function to send.
614 /// @param value Function-specific value (degrees C, a HeatingMode as float, 0/1, or ignored) —
615 /// see encode_heating_payload().
616 /// @return true only if the device answered with CMD_WRITE_PRIVATE_ACK.
617 virtual bool send_heating_command(const std::string &device_id, HeatingFunction fn, float value);
618 /// @brief Queue an async position update; returns immediately, executed in loop().
619 ///
620 /// If a pending SET_TILT operation for the same device is already in the queue, the two are
621 /// coalesced into a single SET_POSITION_AND_TILT command to avoid two radio exchanges.
622 /// This transparently handles Home Assistant sending cover.set_cover_position and
623 /// cover.set_cover_tilt_position as separate rapid calls.
624 /// @param device_id Target device ID.
625 /// @param position Desired position (0–100).
626 virtual void queue_set_device_position(const std::string &device_id, uint8_t position);
627 /// @brief Queue an async named command (STOP, FAVORITE, VENT, FORCE_OPEN); returns immediately,
628 /// executed in loop().
629 ///
630 /// Existing entity/button callers (cover, favorite button, vent button) intentionally ignore
631 /// the return value — they always target a known, already-registered device. It exists so
632 /// force_open_device() can report enqueue rejection distinctly from a queued-but-not-yet-run
633 /// command.
634 /// @param device_id Target device ID.
635 /// @param cmd Named command to send.
636 /// @return true if the hub is initialized, the device is registered, and the command matches
637 /// its capability class (so the command was enqueued); false otherwise.
638 virtual bool queue_device_command(const std::string &device_id, CoverCommand cmd);
639 /// @brief Queue an async tilt update; returns immediately, executed in loop().
640 ///
641 /// If a pending SET_POSITION operation for the same device is already in the queue, the two are
642 /// coalesced into a single SET_POSITION_AND_TILT command to avoid two radio exchanges.
643 /// This transparently handles Home Assistant sending cover.set_cover_position and
644 /// cover.set_cover_tilt_position as separate rapid calls.
645 /// @param device_id Target device ID.
646 /// @param tilt_percent Desired tilt (0–100).
647 virtual void queue_set_device_tilt(const std::string &device_id, uint8_t tilt_percent);
648 /// Queue an async combined position+tilt update; returns immediately, executed in loop().
649 /// @param device_id Target device ID.
650 /// @param position Desired position (0–100).
651 /// @param tilt_percent Desired tilt (0–100).
652 virtual void queue_set_device_position_and_tilt(const std::string &device_id, uint8_t position, uint8_t tilt_percent);
653 /// Queue an async status request; returns immediately, executed in loop().
654 /// @param device_id Target device ID.
655 virtual void queue_request_device_status(const std::string &device_id);
656 /// Queue an async device-name request; returns immediately, executed in loop().
657 /// @param device_id Target device ID.
658 virtual void queue_request_device_name(const std::string &device_id);
659 /// Queue a pairing operation; executed in loop() when radio idle.
660 virtual void queue_discover_and_pair();
661 /// @brief Entry point for the "Scan Paired Devices" button: run the roll-call and publish its
662 /// report to the log and the Home Assistant result event, exactly as the native API action does.
663 ///
664 /// Deliberately not queued through OperationQueue, unlike queue_discover_and_pair(): the
665 /// roll-call has no priority, coalescing or dedup semantics to preserve, and it already runs
666 /// this way from the native API action. What it *is* guarded on is `busy_` — a button, unlike an
667 /// API action, is also reachable from an ESPHome automation, which can fire from inside a
668 /// blocking exchange (an entity callback -> on_value: -> button.press: chain) and would
669 /// otherwise re-enter ExchangeEngine mid-exchange. See ADR 0013 for why one blocking radio
670 /// operation at a time is the whole concurrency model. A press while `busy_` still fires the
671 /// log/event pair (a failed result), matching every other rejected management action rather
672 /// than going silent.
673 ///
674 /// Blocks the ESPHome loop for roughly 3 x `pairing_discovery_wait_ms` and will log the
675 /// "operation took a long time" warning, same as the action — see docs/home_io_control.md.
677 /// Async form of set_light_position() that keeps radio work serialized on the main loop.
678 /// queue_set_light_state() is a thin binary-position wrapper around this.
679 /// @param device_id Target device ID.
680 /// @param position Desired IO position (0-100).
681 virtual void queue_set_light_position(const std::string &device_id, uint8_t position);
682 /// Async form of set_light_state() that keeps radio work serialized on the main loop.
683 /// @param device_id Target device ID.
684 /// @param on Desired on/off state.
685 virtual void queue_set_light_state(const std::string &device_id, bool on);
686 /// Async form of set_switch_state() that keeps radio work serialized on the main loop.
687 /// @param device_id Target device ID.
688 /// @param on Desired on/off state.
689 virtual void queue_set_switch_state(const std::string &device_id, bool on);
690 /// Async form of set_lock_state() that keeps radio work serialized on the main loop.
691 /// @param device_id Target device ID.
692 /// @param locked Desired locked/unlocked state.
693 virtual void queue_set_lock_state(const std::string &device_id, bool locked);
694
695#ifdef IOHOME_LR1121_FIRMWARE_UPDATE
696 /// @brief Entry point for the "Flash LR1121 Radio Firmware" button. Thin forwarder to the
697 /// Lr1121FirmwareUpdateController collaborator (lr1121_firmware_update_controller.h).
698 ///
699 /// Only exists when a `lr1121_firmware_update:` block is configured. See
700 /// lr1121_firmware_update_controller.cpp for the full contract, including the safety invariant
701 /// that every bootloader excursion this triggers must end in either radio_->init() or
702 /// App.safe_reboot() — there is no third option.
703 void trigger_lr1121_firmware_update() { this->lr1121_firmware_update_.trigger(); }
704
705#ifdef IOHOME_LR1121_BOOTLOADER_UPDATE
706 /// @brief Set by the "Allow LR1121 Bootloader Rewrite (Irreversible)" switch's write_state().
707 /// Thin forwarder to the Lr1121FirmwareUpdateController collaborator.
708 ///
709 /// A permission, not an override: this can only convert a cached
710 /// BootloaderUpgradePath::AVAILABLE verdict into "run the three-stage sequence" (bootloader
711 /// ADR 0021) -- it never affects REJECT_WRONG_CHIP, the post-entry sanity
712 /// check, the busy_ guard, or any other verdict. Read once, at button-press time
713 /// (trigger_lr1121_firmware_update()); the ESPHome loop is blocked for the whole three-stage
714 /// sequence once it starts, so the switch cannot change mid-flash. Deliberately not named
715 /// anything with "armed" -- lr1121_flash_confirmation_armed_ already means the two-press window
716 /// this switch *replaces* for its own path, and a reader must never have to guess which is meant.
717 void set_bootloader_rewrite_allowed(bool allowed) {
718 this->lr1121_firmware_update_.set_bootloader_rewrite_allowed(allowed);
719 }
720#endif
721#endif
722
723 protected:
724 // --- Protocol-level operations ---
725 /// Transmit a raw IoFrame on the current frequency with given preamble length.
726 /// @param frame IoFrame to transmit.
727 /// @param freq RF frequency in Hz.
728 /// @param preamble Preamble length in bytes (e.g. `LONG_PREAMBLE`, `SHORT_PREAMBLE`, or a
729 /// tuning-configured value such as `normal_start_preamble`).
730 bool transmit_frame_(const IoFrame &frame, uint32_t freq, uint16_t preamble);
731 /// Main request/response exchange with retry and automatic authentication.
732 /// @param request Outbound request IoFrame.
733 /// @param response Output: received response IoFrame.
734 /// @param freq RF frequency in Hz.
735 /// @param max_tries Transmit-attempt cap, forwarded to ExchangeEngine::send_and_receive().
736 /// @return true if exchange succeeded; false otherwise.
737 ExchangeOutcome send_and_receive_(const IoFrame &request, IoFrame &response, uint32_t freq,
738 uint8_t max_tries = EXCHANGE_RETRY_COUNT);
739 /// Handle an inbound authenticated command from a device (status updates, etc.).
740 /// @param request Inbound authenticated request (e.g., CMD_STATUS_UPDATE).
741 /// @param freq RF frequency the packet arrived on.
742 /// @return true if authentication succeeded; false otherwise.
743 bool authenticate_request_(const IoFrame &request, uint32_t freq);
744 /// Parse a received frame, merge supported device state or metadata, and notify callbacks.
745 /// @param packet Raw radio packet containing a parsed IoFrame.
746 void process_received_packet_(const RadioRxPacket &packet);
747
748 /// True while the key-extraction responder is mid-attempt and still within its bounded CH2-hold
749 /// window. Thin forwarder to KeyExtractionResponder::awaiting_reply() (key_extraction_responder.h)
750 /// — kept on the hub because defer_background_poll_() and tests/hub_core_test.cpp reach it here,
751 /// mirroring the two set_key_extraction_armed* bindings.
752 [[nodiscard]] bool key_extraction_awaiting_reply_() const { return this->key_extraction_.awaiting_reply(); }
753
754 /// Extract supported position or metadata info from a response frame and merge it into the device record.
755 /// @param frame IoFrame containing a supported inbound command such as CMD_PRIVATE_RESP,
756 /// CMD_STATUS_UPDATE, CMD_GET_NAME_RESP, or CMD_GET_INFO2_RESP.
757 /// @param trust_position False to apply `is_stopped` but skip target/position decode for a
758 /// CMD_PRIVATE_RESP — the immediate reply to our own just-sent CMD_EXECUTE echoes stale
759 /// pre-command target/current values on at least some devices (see
760 /// tests/corpus/captures/exchange/somfy_awning_exchange_ack_reports_stale_target_*.yaml), so
761 /// execute_request_and_update_() passes false there; every other caller trusts as before.
762 void update_device_status_(const IoFrame &frame, bool trust_position = true);
763 /// Record that a 1W frame just went out on the radio — ours or someone else's — updating
764 /// last_1w_activity_ms_ and — when this frame starts a new burst (see
765 /// decisions::oneway_burst_started_fresh()) — first_1w_activity_ms_.
766 ///
767 /// Called both from process_received_packet_() for an overheard remote's frame and from
768 /// execute_oneway_command_()/execute_oneway_position_() (hub_operations.cpp) for a frame this
769 /// hub just transmitted itself. That second case is deliberate, not a misuse of a receive-path
770 /// hook: our own burst should defer background polls exactly like a remote's does, because it
771 /// puts the same 1W traffic on the same shared channel the polls would otherwise use, and the
772 /// devices it targets need the same settling time either way.
773 /// @param now millis() at which this frame was seen or sent.
774 void record_1w_activity_(uint32_t now);
775 /// If `frame` matches a 1W remote's pairing gesture (decisions::is_one_way_pairing_gesture()),
776 /// remember it (src/dst/cmd plus the radio's last-capture RSSI) in
777 /// recent_oneway_pairing_sighting_ so a fresh discover_and_pair() attempt can seed its telemetry
778 /// with it — see RecentOneWayPairingSighting's doc comment (issue #27/#65). A no-op for any
779 /// other frame. Called from process_received_packet_()'s 1W-frame path, unconditionally (before
780 /// the burst-dedup check, like record_1w_activity_()) so a repeated gesture frame still
781 /// refreshes the timestamp (and RSSI).
782 /// @param frame Parsed 1W frame (CTRL0 1W bit already confirmed set by the caller).
783 /// @param now millis() at which this frame was seen.
784 void record_oneway_pairing_gesture_(const IoFrame &frame, uint32_t now);
785 /// Schedule a delayed status poll for a registered device using the Component timeout API.
786 /// @param device_id ID of the device to poll.
787 /// @param delay_ms Delay in milliseconds before polling.
788 /// @note Uses ESPHome's set_timeout() mechanism; the callback executes in loop().
789 /// A zero delay schedules immediately on the next loop iteration.
790 void schedule_status_poll_(const std::string &device_id, uint32_t delay_ms);
791 /// Begin bounded follow-up polling for a device after a command or overheard remote activity.
792 /// @param device_id ID of the device to poll.
793 /// @param initial_delay_ms Delay before the first follow-up poll.
794 void begin_status_poll_tracking_(const std::string &device_id, uint32_t initial_delay_ms);
795 /// Arm the confirming poll that follows a command, because a CMD_EXECUTE reply is never trusted
796 /// for position (see update_device_status_()'s trust_position parameter) and therefore leaves the
797 /// hub with no idea where the device actually is. Re-arms the bounded tracking window rather than
798 /// only setting a due time: the same untrusted reply clears that window whenever it claims the
799 /// device is stopped, and pop_due_device() discards a due poll that has no active window. An
800 /// already-scheduled earlier poll wins.
801 /// @param device_id Device the command was sent to.
802 /// @param for_stop True for STOP (and position POS_STOP), which settles under
803 /// STOP_SETTLE_POLL_CAP_MS instead of the normal settle cadence.
804 void arm_execute_confirmation_poll_(const std::string &device_id, bool for_stop);
805 /// Schedule status polls for a fixed list of devices (shared by the id-linked and
806 /// class-linked 1W paths, and by schedule_linked_remote_polls_()).
807 /// @param device_ids Devices to poll.
808 /// @param delay_ms Poll delay in milliseconds.
809 void schedule_device_polls_(const std::vector<std::string> &device_ids, uint32_t delay_ms);
810 /// Whether loop() should skip dispatching the queue this iteration because the pending work is a
811 /// background poll and either a 1W remote transmitted very recently, or a key-extraction attempt
812 /// is mid-flight. Thin wrapper binding the component's state to
813 /// decisions::defer_background_poll_for_1w_activity(), plus a second, independent yield condition:
814 /// a background poll is a blocking exchange that owns the radio for 1-3 s, and dispatching one
815 /// while the key-extraction responder is holding CH2 for an expected CMD_KEY_TRANSFER (0x32)
816 /// would swallow it just as thoroughly as a mistimed hop — see loop()'s hop branch (hub_core.cpp)
817 /// for the other half of that hold. Only background polls yield here, same as the 1W rule: a user
818 /// command must never wait on either kind of background activity.
819 [[nodiscard]] bool defer_background_poll_() const {
820 const bool next_op_is_background =
821 !this->op_queue_.empty() && OperationQueue::is_background_op(this->op_queue_.front().type);
822 if (next_op_is_background && this->key_extraction_awaiting_reply_())
823 return true;
825 this->last_1w_activity_ms_, millis(),
827 }
828 /// Schedule status polls for all devices associated with a linked remote.
829 /// @param remote_id Source node ID of the remote.
830 /// @param delay_ms Poll delay; default REMOTE_ACTIVITY_STATUS_POLL_DELAY_MS. A STOP intent
831 /// passes 0 (position settles immediately, no need to wait out the usual travel-time
832 /// assumption behind the default delay).
833 void schedule_linked_remote_polls_(const std::string &remote_id,
834 uint32_t delay_ms = REMOTE_ACTIVITY_STATUS_POLL_DELAY_MS);
835 /// Resolve the set of devices a 1W frame should affect: devices linked to the sending remote
836 /// by node ID, plus — when the frame targets a typed broadcast (e.g. "all awnings") — devices
837 /// linked to that device class, deduplicated so a device linked both ways is touched once.
838 /// @param info Already-decoded 1W frame info (see decode_1w_frame()).
839 /// @param src_id Sender's node ID as a string (already computed by the caller).
840 /// @return Deduplicated device IDs (may be empty).
841 [[nodiscard]] std::vector<std::string> resolve_1w_target_devices_(const OneWayFrameInfo &info,
842 const std::string &src_id) const;
843 /// Apply optimistic target state to every device in @p device_ids, when the decoded frame
844 /// carries a resolvable intent. Skips devices whose known type doesn't match the frame's
845 /// typed-broadcast target (an "all awnings" press must not optimistically move a linked
846 /// shutter — it is still polled by schedule_device_polls_()). No-op per device when that
847 /// device has `optimistic_state == false` (see DeviceRegistry::apply_optimistic_target()).
848 /// @param info Already-decoded 1W frame info (see decode_1w_frame()).
849 /// @param device_ids Devices to apply optimistic state to (see resolve_1w_target_devices_()).
850 /// @return true if the intent resolved to a STOP (caller should poll immediately).
851 bool apply_optimistic_linked_state_(const OneWayFrameInfo &info, const std::vector<std::string> &device_ids);
852 /// Fire the sender HA event for a decoded 1W frame, if the sender is exposed.
853 /// DEBUG-logs the reason when it does not fire (API disconnected / sender not exposed) so a
854 /// live log capture can distinguish "never reached this check" from "reached it and skipped".
855 /// @param info Already-decoded 1W frame info (see decode_1w_frame()).
856 /// @param linked True if the sender is linked to at least one registered device.
857 /// @param src_id Sender's node ID as a string (already computed by the caller).
858 void maybe_fire_sender_event_(const OneWayFrameInfo &info, bool linked, const std::string &src_id);
859 /// Handle an explicit CMD_ERROR_RESP refusal from the device: record the result code, stamp link
860 /// health, and schedule the poll backoff. Split out of execute_request_and_update_() to keep that
861 /// function's outcome dispatch readable — a refusal is a distinct concern from "what did the
862 /// exchange achieve".
863 /// @param device_id Target device ID.
864 /// @param request Outbound request frame that drew the refusal.
865 /// @param response The CMD_ERROR_RESP frame.
866 /// @param retry_after_fail_ms If non-zero, schedules next status poll after this delay.
867 /// @return Always false; a refusal is never a success.
868 bool handle_error_response_(const std::string &device_id, const IoFrame &request, const IoFrame &response,
869 uint32_t retry_after_fail_ms);
870
871 /// Shared request/response helper for high-level operations.
872 /// @param device_id Target device ID.
873 /// @param request Outbound request frame.
874 /// @param warn_on_no_response If true, logs a warning when no response is received.
875 /// @param retry_after_fail_ms If non-zero, schedules next status poll after this delay on failure.
876 /// @param max_tries Transmit-attempt cap, forwarded to send_and_receive_(). Defaults to the full
877 /// EXCHANGE_RETRY_COUNT; a scheduler-owned poll passes SCHEDULED_POLL_MAX_TRIES.
878 /// @return true when the device replied, or when a CMD_EXECUTE was accepted without a reply —
879 /// every other command's unconfirmed acceptance is still a failure here (see
880 /// @ref ExchangeOutcome and decisions::retry_after_unconfirmed_accept_is_safe()).
881 bool execute_request_and_update_(const std::string &device_id, const IoFrame &request, bool warn_on_no_response,
882 uint32_t retry_after_fail_ms = 0, uint8_t max_tries = EXCHANGE_RETRY_COUNT);
883
884 /// @brief Everything one execute-family operation needs beyond its own guard and frame builder.
886 const char *action; ///< Verb/phrase for the "Sending ..." and rejection logs.
887 bool settle_as_stop; ///< Passed through to arm_execute_confirmation_poll_().
888 };
889 /// Funnel for the four execute-family operations: runs try_execute_operation_() and, on any
890 /// false return (the command will not reach the device), withdraws the optimistic prediction the
891 /// entity applied at control() time via DeviceRegistry::rollback_optimistic(). Wrapping rather
892 /// than inlining the rollback keeps every current and future failure exit covered by one call.
893 /// @param device_id Target device ID.
894 /// @param spec Pre-formatted action phrase and the settle-as-stop flag.
895 /// @param accepts Capability guard; returns false to reject the operation for this device.
896 /// @param rejection_profile Expected-profile label for the rejection log.
897 /// @param build Fills the request frame from the resolved device; returns false on failure.
898 /// @return true when the exchange succeeded and the settle poll was armed.
899 bool run_execute_operation_(const std::string &device_id, const ExecuteRequestSpec &spec,
900 const std::function<bool(const IoDevice &)> &accepts, const char *rejection_profile,
901 const std::function<bool(IoFrame &, const IoDevice &)> &build);
902
903 /// Shared skeleton for the four execute-family operations (position, named command, tilt,
904 /// position+tilt): device lookup + initialized guard, capability guard, poll-tracking start,
905 /// "Sending ..." log, frame build, exchange, failure backoff, and the settle poll. The four
906 /// public methods supply only their guard predicate, rejection profile label, and frame
907 /// builder. Called only through run_execute_operation_(), which owns the failure rollback.
908 /// @param device_id Target device ID.
909 /// @param spec Pre-formatted action phrase and the settle-as-stop flag.
910 /// @param accepts Capability guard; returns false to reject the operation for this device.
911 /// @param rejection_profile Expected-profile label for the rejection log.
912 /// @param build Fills the request frame from the resolved device; returns false on failure.
913 /// @return true when the exchange succeeded and the settle poll was armed.
914 bool try_execute_operation_(const std::string &device_id, const ExecuteRequestSpec &spec,
915 const std::function<bool(const IoDevice &)> &accepts, const char *rejection_profile,
916 const std::function<bool(IoFrame &, const IoDevice &)> &build);
917
918 /// Execute a named device command (STOP, FAVORITE, VENT, FORCE_OPEN) via the authenticated exchange.
919 /// @param device_id Target device ID.
920 /// @param cmd Named command to execute.
921 /// @return true if device acknowledged; false otherwise.
922 bool execute_device_command_(const std::string &device_id, CoverCommand cmd);
923 /// Shared bookkeeping for every 1W transmit: mark the radio busy for the duration of `send`,
924 /// then record it as 1W activity so background polls back off for it exactly as they do for a
925 /// remote's burst — the radio is equally busy either way. Every 1W execute must go through this;
926 /// a future one that skips it would compile, pass, and silently break poll-deferral.
927 /// @param send Callable that performs the actual transmit; takes no arguments.
928 template<typename F> void execute_oneway_(F &&send) {
929 this->busy_ = true;
930 send();
931 this->busy_ = false;
932 this->record_1w_activity_(millis());
933 }
934 /// Send a queued 1W named command. Unlike its 2W sibling this returns nothing: there is no
935 /// acknowledgement to report, and success here would only mean "bytes left the radio".
936 /// @param controller_id Controller-identity handle.
937 /// @param cmd Named command to send.
938 void execute_oneway_command_(const std::string &controller_id, CoverCommand cmd);
939 /// Send a queued 1W numeric position. See execute_oneway_command_().
940 /// @param controller_id Controller-identity handle.
941 /// @param position Target position 0–100.
942 void execute_oneway_position_(const std::string &controller_id, uint8_t position);
943 /// Send a queued 1W enrollment (add-controller). See execute_oneway_command_().
944 /// @param controller_id Controller-identity handle.
945 void execute_oneway_enroll_(const std::string &controller_id);
946 /// Send a queued 1W un-enrollment (remove-controller). See execute_oneway_command_().
947 /// @param controller_id Controller-identity handle.
948 void execute_oneway_unenroll_(const std::string &controller_id);
949 /// Fire all registered device update callbacks for the given device ID.
950 /// @param id Device ID that updated.
951 void notify_device_update_(const std::string &id);
952 /// Apply backoff after a failed background status poll and log the result.
953 /// @param device_id Target device ID.
954 /// @param auth_like True when the failed exchange saw a 0x3C challenge.
955 void schedule_background_poll_backoff_(const std::string &device_id, bool auth_like);
956 /// Pop next pending operation from the queue and execute it (set position, request status, discover).
958
959 // --- Exchange helpers (thin wrappers delegating to ExchangeEngine) ---
960
961 /// Log the last exchange debug snapshot (delegates to exchange_engine_).
962 void log_exchange_debug_(const char *device_id) const { this->exchange_engine_.log_debug(device_id); }
963
964 // --- Tuning ---
965 /// Apply the current tuning configuration to the active radio driver.
967
968 // --- Management actions (thin wrappers delegating to management_actions_) ---
969 /// Register hub-level Home Assistant actions; called from setup().
970 void register_management_actions_() { this->management_actions_.register_actions(); }
971 /// Native API callback: rename a registered device.
972 void api_rename_device_(const std::string &device_id, const std::string &new_name) {
973 this->management_actions_.api_rename_device(device_id, new_name);
974 }
975 /// Native API callback: trigger a registered device's physical identify.
976 void api_identify_device_(const std::string &device_id) { this->management_actions_.api_identify_device(device_id); }
977 /// Native API callback: force-open a registered cover device.
978 void api_force_open_device_(const std::string &device_id) {
979 this->management_actions_.api_force_open_device(device_id);
980 }
981 /// Native API callback: broadcast a roll-call scan of already-paired devices.
982 void api_scan_paired_devices_() { this->management_actions_.api_scan_paired_devices(); }
983 /// Native API callback: queue a 1W position for a controller identity.
984 void api_oneway_set_position_(const std::string &controller_id, const std::string &position) {
985 this->management_actions_.api_oneway_set_position(controller_id, position);
986 }
987 /// Native API callback: queue a 1W un-enrollment (remove-controller) for a controller identity.
988 void api_oneway_remove_controller_(const std::string &controller_id) {
989 this->management_actions_.api_oneway_remove_controller(controller_id);
990 }
991 /// Native API callback: run a single diagnostic probe against a registered device.
992 void api_probe_device_(const std::string &device_id, const std::string &probe, const std::string &index) {
993 this->management_actions_.api_probe_device(device_id, probe, index);
994 }
995 /// Native API callback: run a bounded diagnostic probe sweep against a registered device.
996 void api_probe_sweep_(const std::string &device_id, const std::string &probe, const std::string &first_index,
997 const std::string &last_index) {
998 this->management_actions_.api_probe_sweep(device_id, probe, first_index, last_index);
999 }
1000 /// Native API callback: run a heating/climate function (CMD_WRITE_PRIVATE 0x20) against a
1001 /// registered climate device.
1002 void api_heating_control_(const std::string &device_id, const std::string &function, const std::string &value) {
1003 this->management_actions_.api_heating_control(device_id, function, value);
1004 }
1005
1006 // --- Frequency hopping ---
1007 void hop_frequency_();
1008
1009 // --- Radio driver selection (called once from setup()) ---
1010 /// Select and construct the radio driver named by the required `radio_type` config field.
1011 ///
1012 /// Kept as its own method rather than inlined into setup(): the three-way chip branch
1013 /// (SX1276/SX1262/LR1121) is enough logic on its own that folding it into setup() pushes
1014 /// that function's cognitive complexity past clang-tidy's threshold.
1015 /// Validates the pins each driver needs and logs a clear error (without calling
1016 /// mark_failed() itself — the caller decides how to react) when a required pin is
1017 /// missing. On success, `*chip_name_out` is set to a static string naming the selected
1018 /// chip (used for logging), and the returned pointer is the heap-allocated (not yet
1019 /// initialized) driver instance.
1020 /// @param chip_name_out Output: human-readable chip name for logging (always set,
1021 /// even on failure, to the best-known name for error messages).
1022 /// @return Newly allocated RadioDriver, or nullptr if pin validation or allocation failed.
1023 RadioDriver *select_and_construct_radio_(const char **chip_name_out);
1024
1025 /// @brief Emit the 1W controller identities to the config dump — node, class, and the resolved
1026 /// ACEI / broadcast (ADR 0031). Factored out of dump_config() to keep its cognitive complexity
1027 /// under the clang-tidy threshold.
1029
1030#ifdef IOHOME_LR1121_FIRMWARE_UPDATE
1031 // --- LR1121 firmware update: thin forwarders to the Lr1121FirmwareUpdateController collaborator
1032 // (lr1121_firmware_update_controller.h). setup()/dump_config() keep calling these names; the
1033 // orchestration and every safety invariant live in the collaborator's .cpp. ---
1034 /// Boot-time bootloader-version excursion — forwards. Called from setup() after
1035 /// select_and_construct_radio_() and before radio_->init().
1036 void run_lr1121_boot_time_bootloader_read_() { this->lr1121_firmware_update_.run_boot_time_bootloader_read(); }
1037 /// Compute and cache the flash verdict once radio_->init() has produced (or failed to produce)
1038 /// an installed-firmware-version read — forwards. Called from setup() regardless of whether
1039 /// init() succeeded.
1040 void cache_lr1121_flash_verdict_() { this->lr1121_firmware_update_.cache_flash_verdict(); }
1041 /// Emit the bootloader version and cached flash verdict to the config dump — forwards. Called
1042 /// from dump_config(), next to the existing radio_->dump_debug() call.
1043 void dump_lr1121_firmware_update_debug_() const { this->lr1121_firmware_update_.dump_debug(); }
1044#endif
1045
1046 // --- Radio driver ---
1048
1049 // --- Hardware pins (set by YAML codegen, passed to radio driver in setup) ---
1050 InternalGPIOPin *rst_pin_{nullptr};
1051 InternalGPIOPin *dio0_pin_{nullptr}; ///< SX1276 DIO0 interrupt
1052 InternalGPIOPin *dio4_pin_{nullptr}; ///< SX1276 DIO4 preamble detect (optional)
1053 InternalGPIOPin *dio1_pin_{nullptr}; ///< SX1262 DIO1 interrupt; also carries the LR1121's DIO9 IRQ line
1054 InternalGPIOPin *busy_pin_{nullptr}; ///< SX1262/LR1121 BUSY pin
1055 InternalGPIOPin *fem_en_pin_{nullptr}; ///< Front-end module enable
1056 InternalGPIOPin *vfem_pin_{nullptr}; ///< Front-end module power
1057 InternalGPIOPin *fem_pa_pin_{nullptr}; ///< Front-end module PA switch
1058
1059 // --- Configuration (from YAML) ---
1060 std::string node_id_str_;
1061 std::string system_key_str_;
1062 std::string radio_type_; ///< "sx1276", "sx1262", or "lr1121"; required by the YAML schema.
1067 uint8_t tcxo_voltage_{DEFAULT_TCXO_VOLTAGE_SETTING_1P8V}; ///< SX1262/LR1121 TCXO voltage setting (default 1.8 V)
1068
1069 // --- Runtime state ---
1070 bool initialized_{false};
1071 bool busy_{false};
1072 bool radio_test_mode_{false}; ///< When true, loop() is suspended for loopback testing.
1073 TuningConfig tuning_{}; ///< Runtime tuning overrides.
1075 /// 1W sender node IDs (remotes or sensors) allowed to fire the sender HA event
1076 /// (`add_exposed_sender`). Config-time list (populated once from YAML), not a per-frame allocation.
1077 std::vector<std::string> exposed_senders_;
1078 /// Invoked once after every pairing attempt completes; see set_pairing_result_callback().
1079 std::function<void()> pairing_result_callback_;
1080 /// Whether diagnostic probes (ManagementActions::probe_device()/probe_sweep()) are enabled.
1081 /// False by default so a build that didn't opt in via `diagnostic_probes: true` never sends an
1082 /// undecoded probe opcode. See set_diagnostic_probes_enabled().
1086 PairingTelemetry pairing_telemetry_; ///< Per-attempt pairing telemetry, shared with ExchangeEngine/PairingEngine.
1087 /// Most recent 1W pairing-gesture frame seen on the hub's normal passive RX path (e.g. a PROG
1088 /// press's WRITE_PRIVATE/1W-remove/discover-alt broadcast), remembered so PairingEngine can seed
1089 /// a fresh discover_and_pair() attempt's telemetry with it — see record_oneway_pairing_gesture_()
1090 /// and RecentOneWayPairingSighting's doc comment (issue #27/#65). Declared before pairing_engine_,
1091 /// which holds a reference to it, so member-init order matches the initializer list.
1093 ExchangeEngine exchange_engine_; ///< Owns all authenticated exchange and LBT/hop logic.
1094 PairingEngine pairing_engine_; ///< Owns the three-phase device pairing flow.
1095 ManagementActions management_actions_; ///< Owns rename, identify, force-open, scan_paired_devices, and other
1096 ///< hub-level HA actions.
1097 /// Owns the 1W controller identities, their rolling-sequence counters and the transmit burst.
1098 /// The third collaborator that drives the radio (ADR 0004), and the only one that awaits
1099 /// nothing — 1W has no reply to wait for.
1101 /// Opt-in, receive-only 1W controller-key adoption listener (oneway_key_adoption.cpp). Armed via
1102 /// the "Recover 1W Controller Key" switch; observes an overheard CMD_ONEWAY_ADD_CONTROLLER and
1103 /// reports the key once, then disarms. Declared after oneway_transmitter_ so member-init order
1104 /// matches the initializer list.
1106 /// Device-role responder for the "Recover System Key" feature (key_extraction_responder.cpp).
1107 /// Owns the pairing_responder::ResponderContext, the throwaway-ID/auto-off/grace-window
1108 /// machinery, and the device-role reply TX. Declared after registry_/radio_/tuning_/node_id_
1109 /// (which it references) and after oneway_key_adoption_ so member-init order matches the
1110 /// initializer list.
1112#ifdef IOHOME_LR1121_FIRMWARE_UPDATE
1113 /// Orchestrates the compile-gated LR1121 transceiver-firmware-update feature
1114 /// (lr1121_firmware_update_controller.cpp): boot-time bootloader read, cached flash verdict,
1115 /// two-press confirmation window, and the button-triggered flash/bootloader-rewrite sequences.
1116 /// Guarded member AND guarded initializer — an unguarded initializer for a guarded member is the
1117 /// classic way this breaks only under make firmware-test.
1118 Lr1121FirmwareUpdateController lr1121_firmware_update_;
1119#endif
1120 /// Subscribers to the per-command 1W report; one per "Last 1W Command" sensor.
1121 std::vector<OneWayCommandReportFn> oneway_report_callbacks_;
1122
1123 /// Identity of the last processed 1W frame, for burst suppression; see
1124 /// decisions::is_duplicate_1w_frame() for why the intent bytes are part of the key.
1126 /// millis() of the most recent 1W frame of any kind, including ones dropped as duplicates —
1127 /// a repeat still means the remote is transmitting. 0 until the first is seen. Gates background
1128 /// polls in loop(); see decisions::defer_background_poll_for_1w_activity().
1130 /// millis() of the first 1W frame in the current burst. Advances to the new frame's timestamp
1131 /// whenever the gap since last_1w_activity_ms_ reaches ONEWAY_QUIET_PERIOD_MS (the previous burst
1132 /// has already released any deferred poll, so this one starts fresh); otherwise holds at the
1133 /// burst's start. Bounds defer_background_poll_() via ONEWAY_POLL_DEFER_CAP_MS.
1135};
1136
1137// ----------------------------------------------------------------------------
1138// Test-visible helpers (inline for host unit tests)
1139// ----------------------------------------------------------------------------
1140
1141/// Check if a stored node ID is valid (not all-zero, not all-0xFF).
1142/// @param id 3‑byte node ID buffer.
1143/// @return true if the ID is non-zero and non-0xFF.
1144inline bool stored_node_id_is_valid(const uint8_t id[NODE_ID_SIZE]) {
1145 bool all_zero = true;
1146 bool all_ff = true;
1147 for (uint8_t i = 0; i < NODE_ID_SIZE; i++) {
1148 all_zero = all_zero && id[i] == 0;
1149 all_ff = all_ff && id[i] == UINT8_MAX;
1150 }
1151 return !all_zero && !all_ff;
1152}
1153
1154/// Format a position float as a human‑readable string (e.g. "50%", "unknown").
1155/// @param pos Position value (0–100 or UNKNOWN_POSITION).
1156/// @return String like "50%" or "unknown".
1157inline std::string format_position(float pos) {
1158 if (pos == UNKNOWN_POSITION) {
1159 return "unknown";
1160 }
1161 char buf[POSITION_TEXT_BUFFER_SIZE];
1162 snprintf(buf, sizeof(buf), "%.0f%%", pos);
1163 return buf;
1164}
1165
1166} // namespace home_io_control
1167} // namespace esphome
Owns the per-hub device table, update callbacks, and linked-remote associations.
InternalGPIOPin * fem_en_pin_
Front-end module enable.
Definition hub_core.h:1055
InternalGPIOPin * fem_pa_pin_
Front-end module PA switch.
Definition hub_core.h:1057
std::string describe_last_commander(const IoDevice &dev) const
Render a device's "last commanded by" string, resolving this hub's own node ID.
void send_oneway_command(const std::string &controller_id, CoverCommand cmd)
Queue a 1W named command, sent as the given controller identity.
Definition hub_core.h:310
virtual bool set_lock_state(const std::string &device_id, bool locked)
Semantic lock helper for lock entities.
virtual ManagementActionResult scan_paired_devices()
Broadcast a roll-call and report every device that answers (see ManagementActions::scan_paired_device...
Definition hub_core.h:527
void dump_oneway_controllers_config_() const
Emit the 1W controller identities to the config dump — node, class, and the resolved ACEI / broadcast...
Definition hub_core.cpp:424
void set_tx_power(uint8_t power)
Set transmit power (dBm).
Definition hub_core.h:193
InternalGPIOPin * dio4_pin_
SX1276 DIO4 preamble detect (optional).
Definition hub_core.h:1052
virtual void set_key_extraction_armed(bool armed)
Arm or disarm the "Recover System Key" (key extraction) responder.
Definition hub_core.h:390
bool execute_request_and_update_(const std::string &device_id, const IoFrame &request, bool warn_on_no_response, uint32_t retry_after_fail_ms=0, uint8_t max_tries=EXCHANGE_RETRY_COUNT)
Shared request/response helper for high-level operations.
void api_probe_device_(const std::string &device_id, const std::string &probe, const std::string &index)
Native API callback: run a single diagnostic probe against a registered device.
Definition hub_core.h:992
void add_exposed_sender(const std::string &sender_id)
Allow a 1W sender (identified by its node ID) to fire the esphome.home_io_control_sender_event event ...
Definition hub_core.h:284
void api_force_open_device_(const std::string &device_id)
Native API callback: force-open a registered cover device.
Definition hub_core.h:978
void execute_oneway_position_(const std::string &controller_id, uint8_t position)
Send a queued 1W numeric position.
void maybe_fire_sender_event_(const OneWayFrameInfo &info, bool linked, const std::string &src_id)
Fire the sender HA event for a decoded 1W frame, if the sender is exposed.
void set_node_id(const std::string &id)
Set the controller's node ID (hex string).
Definition hub_core.h:189
virtual bool set_device_position_and_tilt(const std::string &device_id, uint8_t position, uint8_t tilt_percent)
Set both position and tilt of a tilt-capable cover in one atomic command.
void add_oneway_controller(const OneWayControllerIdentity &identity)
Register a configured 1W controller identity (see oneway_controller.h).
Definition hub_core.h:292
virtual bool set_device_tilt(const std::string &device_id, uint8_t tilt_percent)
Send a tilt command to a tilt‑capable cover.
InternalGPIOPin * dio1_pin_
SX1262 DIO1 interrupt; also carries the LR1121's DIO9 IRQ line.
Definition hub_core.h:1053
void execute_oneway_enroll_(const std::string &controller_id)
Send a queued 1W enrollment (add-controller).
IOHomeControlComponent()
Initialize ExchangeEngine, PairingEngine, and ManagementActions with double-pointer/ reference indire...
Definition hub_core.h:95
uint32_t last_1w_activity_ms_
millis() of the most recent 1W frame of any kind, including ones dropped as duplicates — a repeat sti...
Definition hub_core.h:1129
virtual bool send_heating_command(const std::string &device_id, HeatingFunction fn, float value)
The single hub-side transmit path for 2W heating/climate control (CMD_WRITE_PRIVATE 0x20).
void send_oneway_position(const std::string &controller_id, uint8_t position)
Queue a 1W numeric position, sent as the given controller identity.
Definition hub_core.h:317
virtual bool set_switch_state(const std::string &device_id, bool on)
Semantic binary helper for switch entities.
bool run_execute_operation_(const std::string &device_id, const ExecuteRequestSpec &spec, const std::function< bool(const IoDevice &)> &accepts, const char *rejection_profile, const std::function< bool(IoFrame &, const IoDevice &)> &build)
Funnel for the four execute-family operations: runs try_execute_operation_() and, on any false return...
void api_scan_paired_devices_()
Native API callback: broadcast a roll-call scan of already-paired devices.
Definition hub_core.h:982
void set_rst_pin(InternalGPIOPin *pin)
Set the radio reset pin.
Definition hub_core.h:173
virtual bool apply_optimistic_stop(const std::string &device_id)
Predict that a device has stopped (e.g.
Definition hub_core.h:258
void arm_execute_confirmation_poll_(const std::string &device_id, bool for_stop)
Arm the confirming poll that follows a command, because a CMD_EXECUTE reply is never trusted for posi...
virtual void queue_set_device_tilt(const std::string &device_id, uint8_t tilt_percent)
Queue an async tilt update; returns immediately, executed in loop().
void execute_oneway_(F &&send)
Shared bookkeeping for every 1W transmit: mark the radio busy for the duration of send,...
Definition hub_core.h:928
void begin_status_poll_tracking_(const std::string &device_id, uint32_t initial_delay_ms)
Begin bounded follow-up polling for a device after a command or overheard remote activity.
void dump_config() override
Dump configuration and radio debug info to the log.
Definition hub_core.cpp:391
void update_tuning_select(const std::string &name, const std::string &value)
Receive a select tuning update from a HA select entity.
Definition hub_core.cpp:219
virtual void register_device_callback(DeviceUpdateCallback cb)
Register a callback invoked when any device updates.
Definition hub_core.h:463
void set_pa_pin(uint8_t pa_pin)
Set PA boost pin configuration.
Definition hub_core.h:195
bool handle_error_response_(const std::string &device_id, const IoFrame &request, const IoFrame &response, uint32_t retry_after_fail_ms)
Handle an explicit CMD_ERROR_RESP refusal from the device: record the result code,...
virtual IoDevice * get_device(const std::string &device_id)
Retrieve a device by ID; returns nullptr if not found.
Definition hub_core.cpp:313
bool diagnostic_probes_enabled() const
Whether diagnostic probes are enabled for this build.
Definition hub_core.h:433
virtual void queue_request_device_status(const std::string &device_id)
Queue an async status request; returns immediately, executed in loop().
void api_probe_sweep_(const std::string &device_id, const std::string &probe, const std::string &first_index, const std::string &last_index)
Native API callback: run a bounded diagnostic probe sweep against a registered device.
Definition hub_core.h:996
void update_tuning_number(const std::string &name, float value)
Receive a numeric tuning update from a HA number entity.
Definition hub_core.cpp:202
void record_oneway_pairing_gesture_(const IoFrame &frame, uint32_t now)
If frame matches a 1W remote's pairing gesture (decisions::is_one_way_pairing_gesture()),...
void spi_enable() override
Enable the SPI bus.
Definition hub_core.h:150
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:307
void set_fem_en_pin(InternalGPIOPin *pin)
Set the front‑end module enable pin.
Definition hub_core.h:183
virtual void set_oneway_key_adoption_armed(bool armed)
Arm or disarm the 1W controller-key adoption listener.
Definition hub_core.h:408
InternalGPIOPin * vfem_pin_
Front-end module power.
Definition hub_core.h:1056
TuningConfig tuning_
Runtime tuning overrides.
Definition hub_core.h:1073
ExchangeOutcome send_and_receive_(const IoFrame &request, IoFrame &response, uint32_t freq, uint8_t max_tries=EXCHANGE_RETRY_COUNT)
Main request/response exchange with retry and automatic authentication.
Definition hub_core.cpp:272
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:289
std::vector< std::string > resolve_1w_target_devices_(const OneWayFrameInfo &info, const std::string &src_id) const
Resolve the set of devices a 1W frame should affect: devices linked to the sending remote by node ID,...
void process_pending_operation_()
Pop next pending operation from the queue and execute it (set position, request status,...
void schedule_status_poll_(const std::string &device_id, uint32_t delay_ms)
Schedule a delayed status poll for a registered device using the Component timeout API.
void hop_frequency_()
Delegate channel hop to ExchangeEngine (which owns last_hop_us_).
Definition hub_core.cpp:264
void add_linked_remote(const std::string &remote_id, const std::string &device_id)
Declare that a remote (identified by its node ID) controls a registered device.
Definition hub_core.h:229
virtual void set_device_dimmable(const std::string &device_id, bool dimmable)
Set a device's dimmable flag (see IoDevice::dimmable).
Definition hub_core.cpp:315
void execute_oneway_unenroll_(const std::string &controller_id)
Send a queued 1W un-enrollment (remove-controller).
float get_tuning_number_value(const std::string &name) const
Current value of a numeric tuning parameter, used to seed a HA number entity on boot.
Definition hub_core.cpp:237
bool defer_background_poll_() const
Whether loop() should skip dispatching the queue this iteration because the pending work is a backgro...
Definition hub_core.h:819
void set_dio1_pin(InternalGPIOPin *pin)
Set the DIO1 interrupt pin (SX1262; also carries the LR1121's DIO9 IRQ line).
Definition hub_core.h:179
virtual ManagementActionResult force_open_device(const std::string &device_id)
Move a cover device to fully open at elevated priority, intended to bypass wind/rain soft locks.
Definition hub_core.h:520
const PairingTelemetry & pairing_telemetry() const
Definition hub_core.h:370
bool diagnostic_probes_enabled_
Whether diagnostic probes (ManagementActions::probe_device()/probe_sweep()) are enabled.
Definition hub_core.h:1083
void update_device_status_(const IoFrame &frame, bool trust_position=true)
Extract supported position or metadata info from a response frame and merge it into the device record...
void api_heating_control_(const std::string &device_id, const std::string &function, const std::string &value)
Native API callback: run a heating/climate function (CMD_WRITE_PRIVATE 0x20) against a registered cli...
Definition hub_core.h:1002
virtual void queue_request_device_name(const std::string &device_id)
Queue an async device-name request; returns immediately, executed in loop().
void set_radio_test_mode(bool active)
Suspend the hub's normal loop (packet processing, hopping, polling).
Definition hub_core.h:166
RecentOneWayPairingSighting recent_oneway_pairing_sighting_
Most recent 1W pairing-gesture frame seen on the hub's normal passive RX path (e.g.
Definition hub_core.h:1092
void set_radio_type(const std::string &type)
Set radio type ("sx1276", "sx1262", or "lr1121"); required by the YAML schema.
Definition hub_core.h:197
void api_oneway_remove_controller_(const std::string &controller_id)
Native API callback: queue a 1W un-enrollment (remove-controller) for a controller identity.
Definition hub_core.h:988
virtual void queue_set_lock_state(const std::string &device_id, bool locked)
Async form of set_lock_state() that keeps radio work serialized on the main loop.
virtual void queue_set_light_position(const std::string &device_id, uint8_t position)
Async form of set_light_position() that keeps radio work serialized on the main loop.
void schedule_device_polls_(const std::vector< std::string > &device_ids, uint32_t delay_ms)
Schedule status polls for a fixed list of devices (shared by the id-linked and class-linked 1W paths,...
void loop() override
Main loop: process pending operations and drive radio state machine.
Definition hub_core.cpp:325
bool try_execute_operation_(const std::string &device_id, const ExecuteRequestSpec &spec, const std::function< bool(const IoDevice &)> &accepts, const char *rejection_profile, const std::function< bool(IoFrame &, const IoDevice &)> &build)
Shared skeleton for the four execute-family operations (position, named command, tilt,...
void log_exchange_debug_(const char *device_id) const
Log the last exchange debug snapshot (delegates to exchange_engine_).
Definition hub_core.h:962
void set_pairing_result_callback(std::function< void()> cb)
Register a callback invoked once, right after every discover_and_pair() attempt completes — used by t...
Definition hub_core.h:376
void schedule_background_poll_backoff_(const std::string &device_id, bool auth_like)
Apply backoff after a failed background status poll and log the result.
Definition hub_core.cpp:295
PairingTelemetry pairing_telemetry_
Per-attempt pairing telemetry, shared with ExchangeEngine/PairingEngine.
Definition hub_core.h:1086
void send_oneway_unenroll(const std::string &controller_id)
Queue a standalone 1W un-enrollment (remove-controller) for the given controller identity,...
Definition hub_core.h:353
OneWayTransmitter oneway_transmitter_
Owns the 1W controller identities, their rolling-sequence counters and the transmit burst.
Definition hub_core.h:1100
virtual ManagementActionResult heating_control(const std::string &device_id, const std::string &function, const std::string &value)
Run one 2W heating/climate function (CMD_WRITE_PRIVATE 0x20) against a registered climate device — th...
Definition hub_core.h:565
void add_oneway_command_report_callback(OneWayCommandReportFn callback)
Subscribe to the report emitted after every 1W command attempt.
Definition hub_core.h:362
std::string radio_type_
"sx1276", "sx1262", or "lr1121"; required by the YAML schema.
Definition hub_core.h:1062
void api_rename_device_(const std::string &device_id, const std::string &new_name)
Native API callback: rename a registered device.
Definition hub_core.h:972
float get_setup_priority() const override
Get setup priority (HARDWARE to initialize early).
Definition hub_core.h:146
void record_1w_activity_(uint32_t now)
Record that a 1W frame just went out on the radio — ours or someone else's — updating last_1w_activit...
bool key_extraction_awaiting_reply_() const
True while the key-extraction responder is mid-attempt and still within its bounded CH2-hold window.
Definition hub_core.h:752
virtual void queue_set_device_position_and_tilt(const std::string &device_id, uint8_t position, uint8_t tilt_percent)
Queue an async combined position+tilt update; returns immediately, executed in loop().
void set_busy_pin(InternalGPIOPin *pin)
Set the BUSY pin (SX1262/LR1121).
Definition hub_core.h:181
virtual bool queue_device_command(const std::string &device_id, CoverCommand cmd)
Queue an async named command (STOP, FAVORITE, VENT, FORCE_OPEN); returns immediately,...
virtual ManagementActionResult identify_device(const std::string &device_id)
Trigger a device's physical identify (brief jog/flash) so a user can confirm which physical motor a d...
Definition hub_core.h:507
virtual bool discover_and_pair()
Discover and pair a device that is in pairing mode.
RadioDriver * select_and_construct_radio_(const char **chip_name_out)
Select and construct the radio driver named by the required radio_type config field.
Definition hub_core.cpp:137
void api_oneway_set_position_(const std::string &controller_id, const std::string &position)
Native API callback: queue a 1W position for a controller identity.
Definition hub_core.h:984
uint8_t spi_read() override
Read one byte (MISO only).
Definition hub_core.h:162
void api_identify_device_(const std::string &device_id)
Native API callback: trigger a registered device's physical identify.
Definition hub_core.h:976
bool transmit_frame_(const IoFrame &frame, uint32_t freq, uint16_t preamble)
Transmit a raw IoFrame on the current frequency with given preamble length.
Definition hub_core.cpp:267
void set_dio0_pin(InternalGPIOPin *pin)
Set the DIO0 interrupt pin (SX1276).
Definition hub_core.h:175
void set_tuning_config(const TuningConfig &config)
Apply the tuning configuration generated from YAML / UI entities.
Definition hub_core.h:202
void send_oneway_action(const std::string &controller_id, OneWayButtonAction action)
Queue whichever of position/command a generated button's action resolves to.
Definition hub_core.h:328
bool radio_test_mode_
When true, loop() is suspended for loopback testing.
Definition hub_core.h:1072
void set_vfem_pin(InternalGPIOPin *pin)
Set the VFEM power pin.
Definition hub_core.h:185
ExchangeEngine exchange_engine_
Owns all authenticated exchange and LBT/hop logic.
Definition hub_core.h:1093
virtual bool apply_optimistic_tilt(const std::string &device_id, float tilt_percent)
Set an optimistic slat angle ahead of a confirming status poll, and notify.
Definition hub_core.h:271
const OneWayControllerRegistry & oneway_controllers() const
Definition hub_core.h:297
virtual ManagementActionResult probe_sweep(const std::string &device_id, const std::string &probe, const std::string &first_index, const std::string &last_index)
Walk a bounded index range, one probe_device() call per index (see ManagementActions::probe_sweep()).
Definition hub_core.h:549
void set_oneway_key_adoption_armed_callback(std::function< void(bool)> cb)
Register a callback invoked whenever the 1W key-adoption armed state changes — manual toggle,...
Definition hub_core.h:415
bool oneway_key_adoption_armed() const
Whether the 1W key-adoption listener is currently armed.
Definition hub_core.h:421
virtual bool set_light_state(const std::string &device_id, bool on)
Semantic binary helper for light entities.
esphome::home_io_control::ManagementActionResult ManagementActionResult
Result payload used by hub-level management actions such as rename.
Definition hub_core.h:136
PairingEngine pairing_engine_
Owns the three-phase device pairing flow.
Definition hub_core.h:1094
virtual void set_device_silent(const std::string &device_id, bool silent)
Select a device's travel profile at runtime (see IOHomeCoverSilentSwitch).
Definition hub_core.cpp:319
InternalGPIOPin * dio0_pin_
SX1276 DIO0 interrupt.
Definition hub_core.h:1051
void set_system_key(const std::string &key)
Set the system key (hex string).
Definition hub_core.h:191
ManagementActions management_actions_
Owns rename, identify, force-open, scan_paired_devices, and other hub-level HA actions.
Definition hub_core.h:1095
virtual void queue_set_light_state(const std::string &device_id, bool on)
Async form of set_light_state() that keeps radio work serialized on the main loop.
void schedule_linked_remote_polls_(const std::string &remote_id, uint32_t delay_ms=REMOTE_ACTIVITY_STATUS_POLL_DELAY_MS)
Schedule status polls for all devices associated with a linked remote.
void spi_write(uint8_t data) override
Write one byte (MOSI only).
Definition hub_core.h:159
std::vector< OneWayCommandReportFn > oneway_report_callbacks_
Subscribers to the per-command 1W report; one per "Last 1W Command" sensor.
Definition hub_core.h:1121
void set_tcxo_voltage(uint8_t voltage)
Set TCXO voltage for SX1262/LR1121 (1.8V / 3.3V).
Definition hub_core.h:199
OnewayKeyAdoption oneway_key_adoption_
Opt-in, receive-only 1W controller-key adoption listener (oneway_key_adoption.cpp).
Definition hub_core.h:1105
void notify_device_update_(const std::string &id)
Fire all registered device update callbacks for the given device ID.
Definition hub_core.cpp:285
virtual void queue_set_device_position(const std::string &device_id, uint8_t position)
Queue an async position update; returns immediately, executed in loop().
virtual ManagementActionResult rename_device(const std::string &device_id, const std::string &new_name)
Rename a device and verify the result by reading the name back.
Definition hub_core.h:499
std::string get_tuning_select_value(const std::string &name) const
Current option string of a select tuning parameter, used to seed a HA select entity on boot.
Definition hub_core.cpp:252
virtual void queue_set_switch_state(const std::string &device_id, bool on)
Async form of set_switch_state() that keeps radio work serialized on the main loop.
void set_dio4_pin(InternalGPIOPin *pin)
Set the DIO4 preamble‑detect pin (SX1276, optional).
Definition hub_core.h:177
decisions::OneWayDedupState last_1w_logged_
Identity of the last processed 1W frame, for burst suppression; see decisions::is_duplicate_1w_frame(...
Definition hub_core.h:1125
void send_oneway_enroll(const std::string &controller_id)
Queue a 1W enrollment for the given controller identity — the enroll button's press handler.
Definition hub_core.h:343
std::function< void()> pairing_result_callback_
Invoked once after every pairing attempt completes; see set_pairing_result_callback().
Definition hub_core.h:1079
virtual bool set_device_position(const std::string &device_id, uint8_t position)
Send a position command to a device.
void setup() override
Initialize hardware (radio and device registry).
Definition hub_core.cpp:57
KeyExtractionResponder key_extraction_
Device-role responder for the "Recover System Key" feature (key_extraction_responder....
Definition hub_core.h:1111
uint8_t spi_transfer(uint8_t data) override
Transfer one byte full‑duplex.
Definition hub_core.h:156
virtual bool request_device_name(const std::string &device_id)
Request the stored device name from a device.
std::vector< std::string > exposed_senders_
1W sender node IDs (remotes or sensors) allowed to fire the sender HA event (add_exposed_sender).
Definition hub_core.h:1077
uint8_t tcxo_voltage_
SX1262/LR1121 TCXO voltage setting (default 1.8 V).
Definition hub_core.h:1067
virtual 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.
Definition hub_core.h:248
void process_received_packet_(const RadioRxPacket &packet)
Parse a received frame, merge supported device state or metadata, and notify callbacks.
bool execute_device_command_(const std::string &device_id, CoverCommand cmd)
Execute a named device command (STOP, FAVORITE, VENT, FORCE_OPEN) via the authenticated exchange.
bool authenticate_request_(const IoFrame &request, uint32_t freq)
Handle an inbound authenticated command from a device (status updates, etc.).
Definition hub_core.cpp:281
void spi_disable() override
Disable the SPI bus.
Definition hub_core.h:152
InternalGPIOPin * busy_pin_
SX1262/LR1121 BUSY pin.
Definition hub_core.h:1054
void trigger_scan_paired_devices()
Entry point for the "Scan Paired Devices" button: run the roll-call and publish its report to the log...
void register_management_actions_()
Register hub-level Home Assistant actions; called from setup().
Definition hub_core.h:970
void apply_tuning_to_radio_()
Apply the current tuning configuration to the active radio driver.
Definition hub_core.cpp:190
virtual void queue_discover_and_pair()
Queue a pairing operation; executed in loop() when radio idle.
uint32_t first_1w_activity_ms_
millis() of the first 1W frame in the current burst.
Definition hub_core.h:1134
virtual bool request_device_status(const std::string &device_id)
Request current status from a device.
void add_linked_remote_class(DeviceType type, const std::string &device_id)
Declare that a device class's typed 1W broadcasts (e.g.
Definition hub_core.h:237
bool apply_optimistic_linked_state_(const OneWayFrameInfo &info, const std::vector< std::string > &device_ids)
Apply optimistic target state to every device in device_ids, when the decoded frame carries a resolva...
virtual ManagementActionResult probe_device(const std::string &device_id, const std::string &probe, const std::string &index)
Send a single diagnostic probe frame to a registered device and report the raw reply (see ManagementA...
Definition hub_core.h:538
void set_key_extraction_armed_callback(std::function< void(bool)> cb)
Register a callback invoked whenever the key-extraction armed state changes — manual toggle,...
Definition hub_core.h:397
virtual bool set_light_position(const std::string &device_id, uint8_t position)
Send an arbitrary IO position (0-100) to a light entity.
void execute_oneway_command_(const std::string &controller_id, CoverCommand cmd)
Send a queued 1W named command.
void set_diagnostic_probes_enabled(bool enabled)
Set whether ManagementActions::probe_device()/probe_sweep() are allowed to run.
Definition hub_core.h:430
void set_fem_pa_pin(InternalGPIOPin *pin)
Set the FEM PA switch pin.
Definition hub_core.h:187
RadioDriver * get_radio() const
Get the underlying radio driver (for diagnostics and test tooling).
Definition hub_core.h:169
Device-role responder for the "Recover System Key" feature.
Encapsulates hub-level management operations exposed as Home Assistant actions.
The configured 1W controller identities, in YAML declaration order.
Sends 1W commands as the repeated bursts real remotes send.
Opt-in, receive-only listener that adopts an overheard 1W controller key.
Serialized pending-operation queue with coalescing, deduplication, and two-band ordering.
static bool is_background_op(PendingOperationType t)
True for background poll types (REQUEST_STATUS, REQUEST_NAME) that yield to control operations.
Owns and drives all three phases of the IO-Homecontrol device pairing flow.
Fixed-size per-attempt telemetry recorder for the pairing flow.
Abstract radio driver for IO-Homecontrol.
Interface for SPI bus access.
Per-hub poll scheduling and failure-backoff policy.
Per-hub device table, update-callback fan-out, and linked-remote map.
Self-contained authenticated exchange engine for IO-Homecontrol 2W.
OneWayActionEncoding encode_oneway_action(OneWayButtonAction action)
Resolve a button action to the call that sends it.
OneWayButtonAction
The command a generated 1W button sends.
Pure transition helpers for hub-owned exchange and pairing frame decisions.
Internal exchange-state model for hub-owned authenticated non‑pairing flows.
Internal pairing-state model for hub‑owned discovery and key‑exchange flows.
"Recover System Key" (key extraction) — device-role responder collaborator.
LR1121 transceiver-firmware-update feature — orchestration collaborator.
Hub-level management operations exposed as Home Assistant actions.
bool defer_background_poll_for_1w_activity(bool next_op_is_background, uint32_t first_1w_activity_ms, uint32_t last_1w_activity_ms, uint32_t now, uint32_t quiet_ms, uint32_t max_defer_ms)
Decide whether to hold back a queued background poll because a 1W remote is still transmitting.
static constexpr uint32_t ONEWAY_QUIET_PERIOD_MS
Hold queued background polls back for this long after any 1W frame, so a poll the hub itself schedule...
static constexpr float UNKNOWN_POSITION
Sentinel value meaning "position is not known yet".
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
Definition proto_sizes.h:20
DeviceType
Device type identifiers reported by IO‑Homecontrol products.
HeatingFunction
Heating functions, one per user-pressable radiator button in the reference.
constexpr size_t POSITION_TEXT_BUFFER_SIZE
Buffer for formatted position strings such as "100%".
Definition hub_core.h:64
std::function< void(const std::string &device_id, const IoDevice &device)> DeviceUpdateCallback
Callback type invoked when a device's state changes.
CoverCommand
Named device commands for cover-type actuators.
static constexpr uint32_t ONEWAY_POLL_DEFER_CAP_MS
Hard cap on how long sustained 1W traffic may hold a background poll back in total,...
std::string format_position(float pos)
Format a position float as a human‑readable string (e.g.
Definition hub_core.h:1157
static constexpr uint32_t REMOTE_ACTIVITY_STATUS_POLL_DELAY_MS
Delay before polling after overheard remote traffic.
ExchangeOutcome
Authenticated exchange engine — outbound and inbound protocol flows.
bool stored_node_id_is_valid(const uint8_t id[NODE_ID_SIZE])
Check if a stored node ID is valid (not all-zero, not all-0xFF).
Definition hub_core.h:1144
std::function< void(const OneWayCommandReport &report)> OneWayCommandReportFn
Invoked once per attempted 1W command, successful or not.
static constexpr uint8_t EXCHANGE_RETRY_COUNT
Attempts per command before reporting failure.
constexpr uint8_t DEFAULT_TCXO_VOLTAGE_SETTING_1P8V
SX1262 DIO3 setting value for a 1.8 V TCXO.
Definition hub_core.h:63
static constexpr uint8_t AES_KEY_SIZE
AES-128 key size.
Definition proto_sizes.h:23
constexpr uint8_t DEFAULT_PA_PIN_PA_BOOST
SX1276 PA_CONFIG selector for the PA_BOOST output path.
Definition hub_core.h:62
constexpr uint8_t DEFAULT_TX_POWER_DBM
Default TX power used unless YAML overrides it.
Definition hub_core.h:61
Controller identities for the one-way (1W) protocol.
Opt-in, receive-only adoption of a 1W installation's controller key.
One-way (1W) transmit collaborator.
Pending-operation queue with per-type coalescing and deduplication.
Device discovery and key-exchange engine for IO-Homecontrol pairing.
Device-name, address-classification and 1W-frame codecs.
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
Pure codec for IO-Homecontrol 2W heating/climate functions (CMD_WRITE_PRIVATE 0x20).
Radio abstraction layer for IO-Homecontrol.
Per-device poll scheduling, failure backoff, and follow-up-poll state machine.
YAML-declared device metadata for registration; defaults match an undeclared device.
Everything one execute-family operation needs beyond its own guard and frame builder.
Definition hub_core.h:885
const char * action
Verb/phrase for the "Sending ..." and rejection logs.
Definition hub_core.h:886
bool settle_as_stop
Passed through to arm_execute_confirmation_poll_().
Definition hub_core.h:887
Runtime state of a paired IO‑Homecontrol device.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:88
Result of a hub-level management action such as rename.
How a OneWayButtonAction reaches the wire.
bool is_position
True when the action is sent as a numeric position.
uint8_t position
Position to send when is_position.
CoverCommand command
Named command to send otherwise.
One configured 1W controller identity.
Decoded representation of a 1W remote frame.
A 1W pairing-gesture frame observed on the hub's normal passive RX path, remembered so a fresh discov...
All runtime tunable parameters for pairing and radio diagnostics.
Key fields of the last processed 1W frame, used to collapse a remote's repeat burst.
Runtime tuning configuration for pairing and radio diagnostics.