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