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"
36#include "radio_interface.h"
37#include "tuning_config.h"
38#include "hub_exchange.h"
39#include "hub_decisions.h"
40#include "hub_pairing.h"
41#include "device_registry.h"
42#include "status_poll_policy.h"
44#include "operation_queue.h"
45#include "exchange_engine.h"
46#include "pairing_engine.h"
47#include "management_actions.h"
49#include "oneway_controller.h"
50#include "oneway_transmitter.h"
51#include "oneway_key_adoption.h"
52// lr1121_firmware_update_controller.h forward-declares FlashDecision / BootloaderUpgradePath /
53// Lr1121FirmwareUpdater so lr1121_firmware_decisions.h and radio_lr1121_firmware_updater.h are
54// NOT pulled into hub_core.h — that header weight stays in the collaborator's .cpp alone.
56#include <map>
57#include <vector>
58#include <functional>
59
60namespace esphome {
61namespace home_io_control {
62
63inline constexpr uint8_t DEFAULT_TX_POWER_DBM = 17; ///< Default TX power used unless YAML overrides it.
64inline constexpr uint8_t DEFAULT_PA_PIN_PA_BOOST = 0x80; ///< SX1276 PA_CONFIG selector for the PA_BOOST output path.
65inline constexpr uint8_t DEFAULT_TCXO_VOLTAGE_SETTING_1P8V = 0x02; ///< 0-based TCXO voltage code for 1.8 V,
66 ///< passed verbatim to the SX1262/LR1121 chip.
67inline constexpr size_t POSITION_TEXT_BUFFER_SIZE = 16; ///< Buffer for formatted position strings such as "100%".
68
69#ifdef IOHOME_LR1121_FIRMWARE_UPDATE
70/// Max value representable by Component::warn_if_blocking_over_ (a centisecond uint8_t, ~2550 ms
71/// ceiling). Raised for a flash excursion so the log fills with progress output rather than
72/// component-blocking warnings — a flash still runs far longer than 2550 ms, so this reduces
73/// warning spam, it cannot eliminate it. Lives here rather than in the collaborator because
74/// warn_if_blocking_over_ is protected on ESPHome's Component and only the initializer-list lambda
75/// below legitimately writes it.
76inline constexpr uint8_t LR1121_FLASH_WARN_BLOCKING_MAX_CS = 255;
77#endif
78
79/// @brief How long after a pairing attempt ends a Discover & Pair press counts as made during it.
80///
81/// Pairing blocks loop() for up to about 20 s, so a press made meanwhile is handled a few milliseconds
82/// after the attempt ends (issue #165: the queued second attempt found nothing, the radiator had left
83/// its pairing mode). A press a person makes after reading the result comes seconds later.
84static constexpr uint32_t PAIRING_STALE_PRESS_MS = 1000;
85
86// ============================================================================
87// Main Component
88// ============================================================================
89
90/// The main IO-Homecontrol component. Manages the protocol layer and delegates
91/// radio operations to a RadioDriver instance.
92///
93/// Inherits SPIDevice so that ESPHome's Python codegen can configure SPI pins.
94/// Implements SpiAccess to provide the radio driver with SPI bus access.
95/// @ingroup hioc_hub
96class IOHomeControlComponent : public Component,
97 public api::CustomAPIDevice,
98 public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
99 spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_8MHZ>,
100 public SpiAccess {
101 public:
102 /// Initialize ExchangeEngine, PairingEngine, and ManagementActions with double-pointer/
103 /// reference indirection so that test assignments (`comp.radio_ = &mock`) propagate
104 /// through all collaborators without calling setup().
110 // Capturing `this` is safe here: the callback is only ever invoked from send_burst(),
111 // long after construction. It injects the *ability* to transmit rather than a reference
112 // to whichever collaborator currently owns the radio. `&tuning_` is read per burst
113 // (never cached), same pattern as exchange_engine_ above, so a live change to
114 // `normal_start_preamble` takes effect on the next 1W command without a reboot.
115 oneway_transmitter_([this](const IoFrame &frame, uint32_t freq,
116 uint16_t preamble) { return this->transmit_frame_(frame, freq, preamble); },
117 &tuning_),
118 // set_timeout() is protected on the real ESPHome Component (only public in the host stub),
119 // so a lambda defined here — with protected access — is the one legitimate caller. See
120 // oneway_key_adoption.h's NamedTimeoutFn.
121 oneway_key_adoption_([this](const char *name, uint32_t delay_ms, std::function<void()> cb) {
122 this->set_timeout(name, delay_ms, std::move(cb));
123 }),
126 [this](const IoFrame &frame, uint32_t freq, uint16_t preamble) {
127 return this->transmit_frame_(frame, freq, preamble);
128 },
129 [this](const char *name, uint32_t delay_ms, std::function<void()> cb) {
130 this->set_timeout(name, delay_ms, std::move(cb));
131 })
132#ifdef IOHOME_LR1121_FIRMWARE_UPDATE
133 // Guarded initializer for the guarded member. `busy_`/`warn_if_blocking_over_` are
134 // protected: the busy pointer is taken here, and the lambda — with protected access — is
135 // the one legitimate writer of warn_if_blocking_over_. `this` doubles as SpiAccess* and as
136 // the self key for App.scheduler.set_timeout(), never to reach a protected member.
137 ,
138 lr1121_firmware_update_(
139 &radio_, this, &rst_pin_, &busy_pin_, &busy_,
140 [this]() { this->warn_if_blocking_over_ = LR1121_FLASH_WARN_BLOCKING_MAX_CS; }, this)
141#endif
142 {
143 // The hub only supplies what it knows about a target; the engine draws its own decisions from
144 // it (the wake belief and the re-send of an unconfirmed movement command). Installed here
145 // rather than in setup() so a component that never runs setup() (the host tests) is wired
146 // exactly like production.
147 this->exchange_engine_.set_target_evidence_provider([this](const uint8_t *dst, decisions::TargetEvidence &out) {
148 const IoDevice *dev = this->registry_.get(node_id_to_string(dst));
149 if (dev == nullptr)
150 return false;
151 out = decisions::target_evidence(*dev);
152 return true;
153 });
154 this->exchange_engine_.set_device_key_provider([this](const uint8_t *node) -> const uint8_t * {
155 const IoDevice *dev = this->registry_.get(node_id_to_string(node));
156 return dev != nullptr && dev->has_system_key ? dev->system_key : nullptr;
157 });
158 this->exchange_engine_.set_wake_evidence_spent_handler([this](const uint8_t *dst) {
159 if (IoDevice *dev = this->registry_.get(node_id_to_string(dst)); dev != nullptr)
161 });
162 }
163
164 /// @brief Result payload used by hub-level management actions such as rename.
165 /// Alias of the standalone esphome::home_io_control::ManagementActionResult struct so that
166 /// callers using the nested name IOHomeControlComponent::ManagementActionResult continue to work.
168
169 /// @brief Initialize hardware (radio and device registry).
170 void setup() override;
171 /// @brief Main loop: process pending operations and drive radio state machine.
172 void loop() override;
173 /// @brief Dump configuration and radio debug info to the log.
174 void dump_config() override;
175 /// @brief Get setup priority (HARDWARE to initialize early).
176 /// @return setup_priority::HARDWARE.
177 [[nodiscard]] float get_setup_priority() const override { return setup_priority::HARDWARE; }
178
179 // --- SpiAccess implementation (delegates to SPIDevice) ---
180 /// @brief Enable the SPI bus.
181 void spi_enable() override { this->enable(); }
182 /// @brief Disable the SPI bus.
183 void spi_disable() override { this->disable(); }
184 /// @brief Transfer one byte full‑duplex.
185 /// @param data Byte to send.
186 /// @return Received byte.
187 uint8_t spi_transfer(uint8_t data) override { return this->transfer_byte(data); }
188 /// @brief Write one byte (MOSI only).
189 /// @param data Byte to send.
190 void spi_write(uint8_t data) override { this->write_byte(data); }
191 /// @brief Read one byte (MISO only).
192 /// @return Received byte.
193 uint8_t spi_read() override { return this->read_byte(); }
194
195 /// @brief Suspend the hub's normal loop (packet processing, hopping, polling).
196 /// Used by loopback test configs to take exclusive control of the radio.
197 void set_radio_test_mode(bool active) { this->radio_test_mode_ = active; }
198
199 /// @brief Get the underlying radio driver (for diagnostics and test tooling).
200 [[nodiscard]] RadioDriver *get_radio() const { return this->radio_; }
201
202 // --- YAML configuration setters (called by generated code) ---
203 /// Set the radio reset pin.
204 void set_rst_pin(InternalGPIOPin *pin) { this->rst_pin_ = pin; }
205 /// Set the DIO0 interrupt pin (SX1276).
206 void set_dio0_pin(InternalGPIOPin *pin) { this->dio0_pin_ = pin; }
207 /// Set the DIO4 preamble‑detect pin (SX1276, optional).
208 void set_dio4_pin(InternalGPIOPin *pin) { this->dio4_pin_ = pin; }
209 /// Set the DIO1 interrupt pin (SX1262; also carries the LR1121's DIO9 IRQ line).
210 void set_dio1_pin(InternalGPIOPin *pin) { this->dio1_pin_ = pin; }
211 /// Set the BUSY pin (SX1262/LR1121).
212 void set_busy_pin(InternalGPIOPin *pin) { this->busy_pin_ = pin; }
213 /// Set the front‑end module enable pin.
214 void set_fem_en_pin(InternalGPIOPin *pin) { this->fem_en_pin_ = pin; }
215 /// Set the VFEM power pin.
216 void set_vfem_pin(InternalGPIOPin *pin) { this->vfem_pin_ = pin; }
217 /// Set the FEM PA switch pin.
218 void set_fem_pa_pin(InternalGPIOPin *pin) { this->fem_pa_pin_ = pin; }
219 /// Set which FEM part fem_pa_pin is wired to (`fem:` in YAML) — selects whether the SX1262
220 /// driver drives that pin per-transmission (GC1109/KCT8103L/XY16P35) or leaves it static-HIGH
221 /// (NONE, the legacy raw-pin behaviour).
222 void set_fem_profile(FemProfile profile) { this->fem_profile_ = profile; }
223 /// Set the controller's node ID (hex string).
224 void set_node_id(const std::string &id) { this->node_id_str_ = id; }
225 /// Set the system key (hex string).
226 void set_system_key(const std::string &key) { this->system_key_str_ = key; }
227 /// Set transmit power (dBm).
228 void set_tx_power(uint8_t power) { this->tx_power_ = power; }
229 /// Set PA boost pin configuration.
230 void set_pa_pin(uint8_t pa_pin) { this->pa_pin_ = pa_pin; }
231 /// Set radio type ("sx1276", "sx1262", or "lr1121"); required by the YAML schema.
232 void set_radio_type(const std::string &type) { this->radio_type_ = type; }
233 /// Set the SX1262/LR1121 TCXO control-voltage code (0-based, `TCXO_VOLTAGE_OPTIONS` in
234 /// `hub_validators.py`: `1_6V`=0x00 .. `3_3V`=0x07), or `TCXO_VOLTAGE_NONE` (0xFF) for a board with a
235 /// bare crystal and no DIO3-controlled TCXO.
236 void set_tcxo_voltage(uint8_t voltage) { this->tcxo_voltage_ = voltage; }
237
238 /// Apply the tuning configuration generated from YAML / UI entities.
239 void set_tuning_config(const TuningConfig &config) { this->tuning_ = config; }
240 /// Receive a numeric tuning update from a HA `number` entity.
241 void update_tuning_number(const std::string &name, float value);
242 /// Receive a select tuning update from a HA `select` entity.
243 void update_tuning_select(const std::string &name, const std::string &value);
244 /// Current value of a numeric tuning parameter, used to seed a HA `number` entity on boot.
245 /// @param name YAML key of the parameter.
246 /// @return Current value, or 0 for an unknown key.
247 [[nodiscard]] float get_tuning_number_value(const std::string &name) const;
248 /// Current option string of a select tuning parameter, used to seed a HA `select` entity on boot.
249 /// @param name YAML key of the parameter.
250 /// @return Current value formatted as its YAML option string, or empty for an unknown key.
251 [[nodiscard]] std::string get_tuning_select_value(const std::string &name) const;
252
253 /// @brief Render a device's "last commanded by" string, resolving this hub's own node ID.
254 ///
255 /// Thin wrapper over detail::describe_last_commander() (entity_helpers.h); exists because the
256 /// hub's node ID is not reachable from a companion entity.
257 /// @param dev Device record to read.
258 /// @return See detail::describe_last_commander().
259 [[nodiscard]] std::string describe_last_commander(const IoDevice &dev) const;
260
261 /// Declare that a remote (identified by its node ID) controls a registered device.
262 /// When activity from this remote is overheard, a status poll is scheduled for the device.
263 /// This is needed for 1W remotes whose destination address differs from the device's 2W ID.
264 /// @param remote_id Node ID of the remote control.
265 /// @param device_id Node ID of the device it controls.
266 void add_linked_remote(const std::string &remote_id, const std::string &device_id) {
267 this->registry_.add_linked_remote(remote_id, device_id);
268 }
269
270 /// Declare that a device class's typed 1W broadcasts (e.g. "all awnings") also apply to
271 /// @p device_id, matching how 1W remotes address a device class rather than a single node.
272 /// @param type Device class the broadcast targets.
273 /// @param device_id Node ID of the device to add to that class.
274 void add_linked_remote_class(DeviceType type, const std::string &device_id) {
275 this->registry_.add_linked_remote_class(type, device_id);
276 }
277
278 /// Set an optimistic target position ahead of a confirming poll/response, and notify.
279 /// No-op when the device is unknown or has `optimistic_state == false`. See
280 /// DeviceRegistry::apply_optimistic_target() for the full contract.
281 /// Virtual (like add_device/get_device) so platform unit tests can substitute a mock registry.
282 /// @param device_id Target device ID.
283 /// @param target_io_position Target position in IO units (0=open, 100=closed).
284 /// @return true if the optimistic state was applied.
285 virtual bool apply_optimistic_target(const std::string &device_id, float target_io_position) {
286 return this->registry_.apply_optimistic_target(device_id, target_io_position);
287 }
288
289 /// Predict that a device has stopped (e.g. on STOP), and notify.
290 /// No-op when the device is unknown or has `optimistic_state == false`. See
291 /// DeviceRegistry::apply_optimistic_stop() for why this records a prediction rather than a clear.
292 /// Restorable: if the STOP then fails, the movement prediction it replaced comes back.
293 /// Virtual (like add_device/get_device) so platform unit tests can substitute a mock registry.
294 /// @param device_id Target device ID.
295 /// @return true if the optimistic stop was applied.
296 virtual bool apply_optimistic_stop(const std::string &device_id) {
297 // Restorable: this is the entity's own STOP, which the hub is about to send and which can fail.
298 return this->registry_.apply_optimistic_stop(device_id, /*restorable=*/true);
299 }
300
301 /// Set an optimistic slat angle ahead of a confirming status poll, and notify.
302 /// No-op when the device is unknown, has `optimistic_state == false`, or is not tilt-capable.
303 /// See DeviceRegistry::apply_optimistic_tilt() for the full contract and for why a tilt
304 /// command cannot rely on its own reply the way a position command can.
305 /// Virtual like the other device-registry accessors so a test double can override it if it
306 /// needs to; MockPlatformHubBase deliberately does not, and exercises the real registry.
307 /// @param device_id Target device ID.
308 /// @param tilt_percent Slat angle in the same percent scale as `IoDevice::tilt` (0-100).
309 /// @return true if the optimistic tilt was applied.
310 virtual bool apply_optimistic_tilt(const std::string &device_id, float tilt_percent) {
311 return this->registry_.apply_optimistic_tilt(device_id, tilt_percent);
312 }
313
314 /// Allow a 1W sender (identified by its node ID) to fire the `esphome.home_io_control_sender_event`
315 /// event to Home Assistant. "Sender" is deliberately broader than "remote": the same 1W broadcast
316 /// mechanism carries handheld/wall remotes and wind/rain sensors alike (they differ only in the
317 /// `originator` byte inside the payload, not in how they address the radio) — see `decode_1w_frame()`.
318 /// Overheard 1W traffic is always DEBUG-logged regardless of this list; this only controls which
319 /// senders are allowed to reach Home Assistant as an event, independent of whether the sender is
320 /// also linked to a device via `add_linked_remote`. Empty by default — a sender must be explicitly
321 /// opted in.
322 /// @param sender_id Node ID of the 1W sender (remote or sensor).
323 void add_exposed_sender(const std::string &sender_id) { this->exposed_senders_.push_back(sender_id); }
324
325 /// Register a configured 1W controller identity (see oneway_controller.h). Called once per
326 /// `oneway_controllers:` entry from generated code. Both the source address and the key are
327 /// already resolved at schema time — a derived address is computed there so a collision with
328 /// the hub's own address or another identity fails the build rather than silently desyncing a
329 /// transmitter at runtime.
330 /// @param identity Fully-resolved controller identity.
332 this->oneway_transmitter_.add_identity(identity);
333 }
334
335 /// @return The configured 1W controller identities.
336 [[nodiscard]] const OneWayControllerRegistry &oneway_controllers() const {
337 return this->oneway_transmitter_.identities();
338 }
339
340 /// @brief Queue a 1W named command, sent as the given controller identity.
341 ///
342 /// Goes through the operation queue like every other radio operation (ADR 0013), so a 1W burst
343 /// can never interleave with a 2W exchange. Unlike a 2W command this reports nothing back: 1W
344 /// has no reply, so a queued command that a device ignores is indistinguishable from one it
345 /// obeyed. The "Last 1W Command" diagnostic reports what was *transmitted*, which is the only
346 /// half of that the hub can know.
347 /// @param controller_id Controller-identity handle from `oneway_controllers:`.
348 /// @param cmd Named command (STOP, FAVORITE, VENT, FORCE_OPEN); PARTIAL and PEDESTRIAN have no 1W
349 /// encoding and are not transmitted.
350 void send_oneway_command(const std::string &controller_id, CoverCommand cmd) {
351 this->op_queue_.enqueue_oneway_command(controller_id, cmd);
352 }
353
354 /// @brief Queue a 1W numeric position, sent as the given controller identity.
355 /// @param controller_id Controller-identity handle from `oneway_controllers:`.
356 /// @param position Target position 0–100 (0 = fully open, 100 = fully closed), as the user means it;
357 /// the transmitter mirrors it on the wire for an inverted identity (effective_inverted()).
358 void send_oneway_position(const std::string &controller_id, uint8_t position) {
359 this->op_queue_.enqueue_oneway_position(controller_id, position);
360 }
361
362 /// @brief Queue whichever of position/command a generated button's action resolves to.
363 ///
364 /// The mapping is applied here, at enqueue time, so the queue only ever holds concrete
365 /// operations — OPEN and CLOSE are positions on the wire, not commands, and nothing downstream
366 /// should have to know that twice.
367 /// @param controller_id Controller-identity handle from `oneway_controllers:`.
368 /// @param action Button action to send.
369 void send_oneway_action(const std::string &controller_id, OneWayButtonAction action) {
370 const OneWayActionEncoding encoding = encode_oneway_action(action);
371 if (encoding.is_position) {
372 this->send_oneway_position(controller_id, encoding.position);
373 } else {
374 this->send_oneway_command(controller_id, encoding.command);
375 }
376 }
377
378 /// @brief Queue a 1W enrollment for the given controller identity — the enroll button's press
379 /// handler.
380 ///
381 /// Sends `0x39` (self-directed) then `0x30`, back to back — see
382 /// OneWayTransmitter::send_enrollment().
383 /// @param controller_id Controller-identity handle from `oneway_controllers:`.
384 void send_oneway_enroll(const std::string &controller_id) { this->op_queue_.enqueue_oneway_enroll(controller_id); }
385
386 /// @brief Queue a standalone 1W un-enrollment (remove-controller) for the given controller
387 /// identity, reached only through the explicitly-named `oneway_remove_controller` native API
388 /// action — the same `0x39` send_oneway_enroll() also fires as its own prelude, but here alone.
389 ///
390 /// @warning **Works only inside the receiver's programming window.** Confirmed on a Somfy Izymo
391 /// dimmer only: put the receiver in its programming mode first, then fire this; with no window
392 /// open it changes nothing. Other receivers are untested. See ADR 0026 § Consequences.
393 /// @param controller_id Controller-identity handle from `oneway_controllers:`.
394 void send_oneway_unenroll(const std::string &controller_id) {
395 this->op_queue_.enqueue_oneway_unenroll(controller_id);
396 }
397
398 /// @brief Subscribe to the report emitted after every 1W command attempt.
399 ///
400 /// A list rather than a single slot: each identity gets its own "Last 1W Command" sensor, and
401 /// each filters the reports down to its own handle.
402 /// @param callback Invoked for every attempt, successful or not.
404 this->oneway_report_callbacks_.push_back(std::move(callback));
405 }
406
407 /// @return The 1W transmit collaborator, for diagnostics and the sequence-resync path.
409
410 /// @return The telemetry recorded for the most recent (or in-progress) pairing attempt.
411 [[nodiscard]] const PairingTelemetry &pairing_telemetry() const { return this->pairing_telemetry_; }
412
413 /// Register a callback invoked once, right after every `discover_and_pair()` attempt
414 /// completes — used by the "Last Pairing Result" text sensor to publish a fresh value.
415 /// Single-slot: only one platform instance is expected per hub.
416 /// @param cb Callable with no arguments.
417 void set_pairing_result_callback(std::function<void()> cb) { this->pairing_result_callback_ = std::move(cb); }
418
419 /// @return How many device events record_device_event_() has recorded since boot.
420 [[nodiscard]] uint32_t device_event_count() const { return this->device_event_count_; }
421
422 /// @return The most recent device event rendered as `#<n> t=<uptime s> <SRC> <cmd> <payload hex>`,
423 /// or an empty string before the first one. The counter makes a repeated identical report a new
424 /// value, which Home Assistant's history needs (it records state changes only).
425 [[nodiscard]] const std::string &last_device_event() const { return this->last_device_event_; }
426
427 /// Record a diagnostic probe's reply on the "Last Device Event" sensor when it is a device event
428 /// report (decisions::is_device_event_report()), such as the event a `subscribe_event` probe draws
429 /// at once. The reply is only recorded, never applied.
430 /// @param reply The probe's reply frame.
431 /// @return true when it was recorded.
432 bool record_probe_reply_event(const IoFrame &reply);
433
434 /// Register a callback invoked after every recorded device event, used by the "Last Device Event"
435 /// text sensor. Single-slot, mirrors set_pairing_result_callback().
436 /// @param cb Callable with no arguments.
437 void set_device_event_callback(std::function<void()> cb) { this->device_event_callback_ = std::move(cb); }
438
439 /// @brief Arm or disarm the "Recover System Key" (key extraction) responder.
440 ///
441 /// Thin forwarder to the KeyExtractionResponder collaborator (key_extraction_responder.h).
442 /// Arming picks a fresh throwaway node ID, resets the pairing_responder state machine to
443 /// ARMED_IDLE, and schedules a 10-minute auto-off. While armed, the 0x28/0x2C/0x31/0x32 branches
444 /// in process_received_packet_() emulate an unpaired device so a user's existing hub can pair to
445 /// it and hand over its node_id/system_key (see pairing_responder.h). Disarming — manual, via
446 /// the HA switch, on successful extraction, or on auto-off — immediately stops those branches
447 /// from responding; it never touches the real device registry or the hub's own node_id_/
448 /// system_key_. Virtual so platform unit tests can substitute a mock hub, matching every other
449 /// queue_*/set_* entry point on this component.
450 /// @param armed Desired state.
451 virtual void set_key_extraction_armed(bool armed) { this->key_extraction_.set_armed(armed); }
452
453 /// Register a callback invoked whenever the key-extraction armed state changes — manual
454 /// toggle, successful extraction, or auto-off timeout — so the switch entity can keep its
455 /// displayed state in sync when the hub disarms itself rather than the user. Single-slot,
456 /// mirrors set_pairing_result_callback().
457 /// @param cb Callable receiving the new armed state.
458 void set_key_extraction_armed_callback(std::function<void(bool)> cb) {
459 this->key_extraction_.set_armed_callback(std::move(cb));
460 }
461
462 /// Arm or disarm the 1W controller-key adoption listener. Thin forwarder to the OnewayKeyAdoption
463 /// collaborator (oneway_key_adoption.h) — while armed, an overheard CMD_ONEWAY_ADD_CONTROLLER
464 /// broadcast is decrypted and reported once, after which the listener disarms itself (one
465 /// adoption per arm). Receive-only: unlike 2W key extraction this never transmits, it only
466 /// listens for a frame a 1W device broadcasts of its own accord. Virtual so platform unit tests
467 /// can substitute a mock hub, matching every other queue_*/set_* entry point on this component.
468 /// @param armed Desired state.
469 virtual void set_oneway_key_adoption_armed(bool armed) { this->oneway_key_adoption_.set_armed(armed); }
470
471 /// Register a callback invoked whenever the 1W key-adoption armed state changes — manual
472 /// toggle, successful adoption, or auto-off timeout — so the switch entity stays in sync when
473 /// the hub disarms itself rather than the user. Single-slot, mirrors
474 /// set_key_extraction_armed_callback().
475 /// @param cb Callable receiving the new armed state.
476 void set_oneway_key_adoption_armed_callback(std::function<void(bool)> cb) {
477 this->oneway_key_adoption_.set_armed_callback(std::move(cb));
478 }
479
480 /// Whether the 1W key-adoption listener is currently armed.
481 /// @return true while armed.
482 [[nodiscard]] bool oneway_key_adoption_armed() const { return this->oneway_key_adoption_.armed(); }
483
484 /// @brief Set whether DiagnosticProbes::probe_device()/probe_sweep() are allowed to run.
485 ///
486 /// Set once from the `diagnostic_probes:` YAML boolean (`__init__.py`); off by default, so a
487 /// build that doesn't opt in never sends an undecoded probe opcode. Not a runtime toggle: there
488 /// is no entity and nothing else calls this after setup — the gate is "was this build
489 /// configured with `diagnostic_probes: true`", not a state a user flips per session.
490 /// @param enabled Desired state.
491 void set_diagnostic_probes_enabled(bool enabled) { this->diagnostic_probes_enabled_ = enabled; }
492
493 /// @brief Whether diagnostic probes are enabled for this build.
494 [[nodiscard]] bool diagnostic_probes_enabled() const { return this->diagnostic_probes_enabled_; }
495
496 // --- Device management (called by platform entities during setup) ---
497 /// Add a device to the registry by device ID only (undeclared/legacy path).
498 /// Type, subtype and optimistic_state default to UNKNOWN / 0 / true, with no overrides; use the
499 /// `DeviceConfig` overload when metadata comes from a YAML declaration.
500 /// @param device_id Hexadecimal node ID string.
501 virtual void add_device(const std::string &device_id);
502 /// Add a device to the registry with full metadata from a YAML declaration.
503 /// @param device_id Hexadecimal node ID string.
504 /// @param cfg Device type/subtype/overrides/optimistic-state metadata.
505 virtual void add_device(const std::string &device_id, const DeviceConfig &cfg);
506 /// Retrieve a device by ID; returns nullptr if not found.
507 /// @param device_id Hexadecimal node ID.
508 /// @return Pointer to IoDevice, or nullptr.
509 virtual IoDevice *get_device(const std::string &device_id);
510 /// True once loop() has seen the radio latch a failure after setup (a runtime BUSY timeout, for
511 /// example). Sticky: a failed driver never recovers without a reboot. Every device's Active Issue
512 /// sensor reports it.
513 [[nodiscard]] bool radio_stopped_responding() const { return this->radio_failure_reported_; }
514 /// Select a device's travel speed at runtime (see IOHomeCoverTravelSpeedSelect and
515 /// IOHomeCoverSilentSwitch). Virtual so platform tests can substitute a mock registry.
516 /// @param device_id Hexadecimal node ID string.
517 /// @param speed Speed profile of the device's position moves.
518 virtual void set_device_travel_speed(const std::string &device_id, TravelSpeed speed);
519 /// Register a callback invoked when any device updates.
520 /// @param cb Callable with signature void(const std::string&, const IoDevice&).
521 virtual void register_device_callback(DeviceUpdateCallback cb) { this->registry_.subscribe(std::move(cb)); }
522 /// Configure the optional follow-up polling interval for a registered device.
523 /// @param device_id Target device ID.
524 /// @param poll_interval_ms Poll interval in milliseconds; zero keeps the legacy one-shot settle poll only.
525 virtual void set_device_status_poll_interval(const std::string &device_id, uint32_t poll_interval_ms);
526 /// Opt a registered device into the periodic rain sensor poll (a read of its minimum limitation).
527 /// @param device_id Target device ID.
528 /// @param interval_ms Poll interval in milliseconds; zero removes the device from the schedule.
529 virtual void set_device_rain_poll_interval(const std::string &device_id, uint32_t interval_ms);
530 /// Opt a registered device into the periodic sun level poll (a subscription to EVENT_LOCAL_SENSOR,
531 /// whose answer carries the value; ADR 0064).
532 /// @param device_id Target device ID.
533 /// @param interval_ms Poll interval in milliseconds; zero removes the device from the schedule.
534 virtual void set_device_sun_poll_interval(const std::string &device_id, uint32_t interval_ms);
535 /// Opt a registered device into the periodic battery level poll (a subscription to
536 /// EVENT_BATTERY_LEVEL_CHANGE, whose answer carries the level; ADR 0064).
537 /// @param device_id Target device ID.
538 /// @param interval_ms Poll interval in milliseconds; zero removes the device from the schedule.
539 virtual void set_device_battery_level_poll_interval(const std::string &device_id, uint32_t interval_ms);
540 /// Opt a registered device into the status refresh at rest: a status read whenever the device has
541 /// not reported its status for a whole interval.
542 /// @param device_id Target device ID.
543 /// @param interval_ms Refresh interval in milliseconds; zero removes the device from the schedule.
544 virtual void set_device_status_refresh_interval(const std::string &device_id, uint32_t interval_ms);
545
546 // --- High-level operations ---
547 /// Send a position command to a device.
548 /// @param device_id Target device ID.
549 /// @param position Desired position, 0–100 (open→closed). Named commands (STOP, FAVORITE,
550 /// VENT) go through execute_device_command_()/create_execute_command() instead.
551 /// @return true if device acknowledged; false on timeout or radio error, or when the device's
552 /// features do not accept the value (an intermediate position on a device without
553 /// device_feature::LEVELS): nothing is sent and the optimistic target is withdrawn.
554 virtual bool set_device_position(const std::string &device_id, uint8_t position);
555 /// Send a tilt command to a tilt‑capable cover.
556 /// @param device_id Target device ID.
557 /// @param tilt_percent Desired tilt (0–100).
558 /// @return true if device acknowledged; false otherwise.
559 virtual bool set_device_tilt(const std::string &device_id, uint8_t tilt_percent);
560 /// Set both position and tilt of a tilt-capable cover in one atomic command.
561 /// @param device_id Target device ID.
562 /// @param position Desired position (0–100, open→closed).
563 /// @param tilt_percent Desired tilt (0–100).
564 /// @return true if device acknowledged; false otherwise.
565 virtual bool set_device_position_and_tilt(const std::string &device_id, uint8_t position, uint8_t tilt_percent);
566 /// Request current status from a device.
567 /// @param device_id Target device ID.
568 /// @return true if status frame was received and processed.
569 virtual bool request_device_status(const std::string &device_id);
570 /// Read the device's minimum limitation and derive its rain sensor state from the reply.
571 ///
572 /// Unauthenticated read (CMD_LIMITATION_STATUS_REQ); the reply is accepted only inside this
573 /// exchange (ADR 0022) and never reaches update_device_status_(), so a poll cannot move the
574 /// device's position or target. A moving device is not asked: the poll is deferred instead.
575 /// @param device_id Target device ID.
576 /// @return true if a usable limitation reply arrived and was applied.
577 virtual bool request_device_limitation(const std::string &device_id);
578 /// Subscribe the device to EVENT_LOCAL_SENSOR and publish the sun value its answer carries.
579 ///
580 /// The answer is accepted only inside this exchange (decisions::is_subscription_answer(),
581 /// ADR 0064) and never reaches the position decode. A moving device is not asked: the poll is
582 /// deferred instead.
583 /// @param device_id Target device ID.
584 /// @return true if an answer of the captured shape arrived and was applied.
585 virtual bool request_device_sun_level(const std::string &device_id);
586 /// Subscribe the device to EVENT_BATTERY_LEVEL_CHANGE and publish the level its answer carries.
587 /// Same rules as request_device_sun_level().
588 /// @param device_id Target device ID.
589 /// @return true if an answer of the captured shape arrived and was applied.
590 virtual bool request_device_battery_level(const std::string &device_id);
591 /// Request the stored device name from a device.
592 /// @param device_id Target device ID.
593 /// @return true if a name response frame was received and processed.
594 virtual bool request_device_name(const std::string &device_id);
595 /// Rename a device and verify the result by reading the name back.
596 /// @param device_id Target device ID.
597 /// @param new_name Requested UTF-8 device name.
598 /// @return Structured result describing success, verification, and any validation failure.
599 virtual ManagementActionResult rename_device(const std::string &device_id, const std::string &new_name) {
600 return this->management_actions_.rename_device(device_id, new_name);
601 }
602 /// Trigger a device's physical identify (brief jog/flash) so a user can confirm which
603 /// physical motor a device ID maps to.
604 /// @param device_id Target device ID.
605 /// @return Structured result describing success and any validation failure. `verified` is
606 /// always false — there is no readback for a physical identify jog.
607 virtual ManagementActionResult identify_device(const std::string &device_id) {
608 return this->management_actions_.identify_device(device_id);
609 }
610 /// @brief Move a cover device to fully open at elevated priority, intended to bypass
611 /// wind/rain soft locks.
612 ///
613 /// Safety-sensitive: queues CoverCommand::FORCE_OPEN through the normal cover-command dispatch
614 /// path. Only confirms the command was queued; the movement outcome arrives later via the
615 /// device's normal cover-state/polling pipeline, so `verified` is always false. The lock-bypass
616 /// behavior itself is experimental and unconfirmed against an active lock — see
617 /// ManagementActions::force_open_device()'s doxygen for details.
618 /// @param device_id Target device ID.
619 /// @return Structured result describing whether the command was queued.
620 virtual ManagementActionResult force_open_device(const std::string &device_id) {
621 return this->management_actions_.force_open_device(device_id);
622 }
623 /// Broadcast a roll-call and report every device that answers (see
624 /// ManagementActions::scan_paired_devices() for the full contract: only key-holding devices
625 /// answer, DeviceRegistry is never written, and zero replies is a successful result).
626 /// @return Structured result whose `message` is the full multi-line report.
628 /// Send a single diagnostic probe frame to a registered device and report the raw reply (see
629 /// DiagnosticProbes::probe_device() for the full contract, argument formats, and safety
630 /// gating). Protocol-research instrumentation for opcodes this codebase has not decoded — see
631 /// docs/diagnostic-probes.md and ADR 0024.
632 /// @param device_id Target device ID.
633 /// @param probe Probe name ("private_fn", "status_ext", "general_info3", "private2",
634 /// "private2_short", "status_mp_fp", "status_fp_high", "limitation", "register_00", or "register_01"; the full
635 /// list is in DiagnosticProbes::probe_device()).
636 /// @param index Function ID / selector block / modifier, as a decimal or `0x`-prefixed hex
637 /// string; ignored for "general_info3" and "status_mp_fp"; the FPI2 bitmap for
638 /// "status_fp_high"; 0 (minimum) or 1 (maximum) for "limitation".
639 /// @return Structured result whose `message` carries the reply's command byte and raw hex.
640 virtual ManagementActionResult probe_device(const std::string &device_id, const std::string &probe,
641 const std::string &index) {
642 return this->management_actions_.probe_device(device_id, probe, index);
643 }
644 /// Walk a bounded index range, one probe_device() call per index (see
645 /// DiagnosticProbes::probe_sweep()).
646 /// @param device_id Target device ID.
647 /// @param probe Probe name, same as probe_device().
648 /// @param first_index First index in the sweep (inclusive).
649 /// @param last_index Last index in the sweep (inclusive).
650 /// @return Structured result whose `message` is one line per index.
651 virtual ManagementActionResult probe_sweep(const std::string &device_id, const std::string &probe,
652 const std::string &first_index, const std::string &last_index) {
653 return this->management_actions_.probe_sweep(device_id, probe, first_index, last_index);
654 }
655 /// @brief Run one 2W heating/climate function (CMD_WRITE_PRIVATE 0x20) against a registered
656 /// climate device — the `heating_control` hub action.
657 ///
658 /// Experimental: the register protocol is unvalidated on real Atlantic/Thermor/Sauter hardware.
659 /// A write is reported as acknowledged by the device, never as confirmed; the register reads
660 /// (`window_open`) put the decoded value into the result message, and `power_on` / `midnight_sync`
661 /// log their ACK payload at DEBUG. See ManagementActions::heating_control() for argument formats.
662 /// @param device_id Target device ID.
663 /// @param function Heating function name.
664 /// @param value Function-specific value string.
665 /// @return Structured result describing success and any validation/exchange failure.
666 virtual ManagementActionResult heating_control(const std::string &device_id, const std::string &function,
667 const std::string &value) {
668 return this->management_actions_.heating_control(device_id, function, value);
669 }
670 /// Discover and pair a device that is in pairing mode.
671 /// @return true if pairing completed successfully; false otherwise.
672 virtual bool discover_and_pair();
673 /// Send an arbitrary IO position (0-100) to a light entity. Internally mapped to the shared
674 /// execute path. set_light_state() is a thin binary-position wrapper around this, used by
675 /// dimmable lights to send anything other than the two binary extremes.
676 /// @param device_id Target device ID.
677 /// @param position Desired IO position (0-100); this device family's convention maps 0 to full
678 /// brightness and 100 to off, the same 0-100 scale platform_cover.cpp uses.
679 /// @return true if device acknowledged.
680 virtual bool set_light_position(const std::string &device_id, uint8_t position);
681 /// Send a heat level to an exterior heater (a fan entity) as an IO position. Mapped to the shared
682 /// execute path, like set_light_position().
683 /// @param device_id Target device ID.
684 /// @param position Desired IO position (0-100): 0 = full heat, 100 = off (the KLF 200 exterior
685 /// heating mapping, MP = position x 0x200).
686 /// @return true if device acknowledged.
687 virtual bool set_fan_position(const std::string &device_id, uint8_t position);
688 /// Semantic binary helper for light entities. Internally mapped to the shared execute path.
689 /// @param device_id Target device ID.
690 /// @param on Desired on/off state.
691 /// @return true if device acknowledged.
692 virtual bool set_light_state(const std::string &device_id, bool on);
693 /// Semantic binary helper for switch entities. Internally mapped to the shared execute path.
694 /// @param device_id Target device ID.
695 /// @param on Desired on/off state.
696 /// @return true if device acknowledged.
697 virtual bool set_switch_state(const std::string &device_id, bool on);
698 /// Semantic lock helper for lock entities. Internally mapped to the shared execute path.
699 /// @param device_id Target device ID.
700 /// @param locked Desired locked/unlocked state.
701 /// @return true if device acknowledged.
702 virtual bool set_lock_state(const std::string &device_id, bool locked);
703 /// @brief Run one heating function on the heater's private register protocol (CMD_WRITE_PRIVATE
704 /// 0x20): a write for a settable register, a read for a read-only one (`power_on`, `midnight_sync`).
705 /// Both the `heating_control` hub action and the climate entity's writes call this; together with
706 /// read_heating_register() it is the only place heating frames are built and sent, through one
707 /// shared exchange helper.
708 ///
709 /// Flow: registry lookup -> device_supports_climate_control() gate (rejected via
710 /// detail::log_rejected_operation()) -> encode_heating_payload() -> create_write_private() (with
711 /// the device's `low_power` flag) -> a plain send_and_receive_(). Deliberately NOT routed
712 /// through execute_request_and_update_(): that helper is cover/position-shaped (status decode,
713 /// poll backoff). A CMD_WRITE_PRIVATE_ACK (0x21) reply is success; anything else (including
714 /// CMD_ERROR_RESP) is failure. The exchange still feeds the device-agnostic Last Contact /
715 /// Exchange Failures link-health sensors. The ack carries no state: a caller that wants the
716 /// value the heater now holds reads it back with read_heating_register(). An acknowledged write
717 /// also moves the heater's next poll to HEATING_POST_WRITE_POLL_DELAY_MS from now, so the regulator
718 /// state (heating / idle) follows the change.
719 /// @param device_id Target device ID (hex string).
720 /// @param fn Heating function to send.
721 /// @param value Function-specific value (degrees C, a HeatingMode as float, 0/1, or ignored) —
722 /// see encode_heating_payload().
723 /// @return true only if the device answered with CMD_WRITE_PRIVATE_ACK.
724 virtual bool send_heating_command(const std::string &device_id, HeatingFunction fn, float value);
725 /// @brief Set the heating level of a heater that is commanded with levels (the plain electrical heater).
726 ///
727 /// A main-parameter-only EXECUTE through the execute-family funnel; the settle poll that follows
728 /// reads the level back with the ordinary status read, and the climate entity shows that reading.
729 /// Experimental: unconfirmed on hardware.
730 /// @param device_id Target device ID (hex string).
731 /// @param level Heating level low byte (heating_level::*).
732 /// @return true when the device accepted the command.
733 virtual bool set_heating_level(const std::string &device_id, uint8_t level);
734 /// @brief Command a heater that carries a temperature or an alias in its main parameter (the heating
735 /// temperature interface and the heat pump).
736 ///
737 /// A main-parameter-only EXECUTE, or one that also carries functional parameter 9 when the profile
738 /// sends mode flags, through the execute-family funnel; the settle poll that follows reads the
739 /// state back with the ordinary status read. Experimental: unconfirmed on hardware.
740 /// @param device_id Target device ID (hex string).
741 /// @param main_parameter The 16-bit main parameter (encode_temperature_main_parameter() or an alias).
742 /// @param fp The functional parameter that accompanies it, if any (kind NONE sends a main-parameter-only
743 /// EXECUTE).
744 /// @return true when the device accepted the command.
745 virtual bool set_heating_temperature(const std::string &device_id, uint16_t main_parameter, const HeatingFp &fp);
746 /// @brief Set a water heater's target temperature.
747 ///
748 /// A main-parameter-only EXECUTE with the target on the relative scale over DHW_TARGET_MIN_C..
749 /// DHW_TARGET_MAX_C (encode_dhw_target()), through the execute-family exchange without the cover's
750 /// movement bookkeeping. The settle poll that follows (poll_dhw_device_()) reads the target back from
751 /// the status reply. Gated on device_feature::WATER_HEATER. The heater's display follows the target
752 /// (issue #198).
753 /// @param device_id Target device ID (hex string).
754 /// @param celsius Target in degrees Celsius; a value outside the heater's range is refused, not clamped.
755 /// @return true when the device accepted the command.
756 virtual bool set_water_heater_target(const std::string &device_id, float celsius);
757 /// @brief Set a water heater's operating mode: a write of register 0x0006 (encode_dhw_mode_write()).
758 ///
759 /// The register is read back at once, so the entity shows what the heater then holds, and so is the
760 /// target (the status read of the poll), which the mode decides. The next poll moves to
761 /// HEATING_POST_WRITE_POLL_DELAY_MS from now so the heating state follows the change. Gated on
762 /// device_feature::WATER_HEATER. The heater's display follows the mode (issue #198).
763 /// @param device_id Target device ID (hex string).
764 /// @param mode Mode to write.
765 /// @return true only if the device acknowledged the write; a missed read-back does not change that.
766 virtual bool set_water_heater_mode(const std::string &device_id, DhwMode mode);
767 /// @brief Start or end a water heater's boost: a duration write of register 0x0000, then an EXECUTE with
768 /// the operating-mode word FP16 (DHW_OPERATING_MODE_BOOST_ON, or DHW_OPERATING_MODE_OFF to end).
769 ///
770 /// See set_dhw_operating_mode_() for the sequence. Ending a boost also ends away, because the off word
771 /// switches both (ADR 0081). Gated on device_feature::WATER_HEATER.
772 /// \todo Experimental: confirm on a real water heater that this starts and ends a boost.
773 /// @param device_id Target device ID (hex string).
774 /// @param on true to start a boost, false to end it.
775 /// @return true when the device accepted the EXECUTE; false for a refused or unacknowledged command.
776 virtual bool set_water_heater_boost(const std::string &device_id, bool on);
777 /// @brief Start or end a water heater's away (holiday) mode: a duration write of register 0x0005 (255, until
778 /// turned off), then an EXECUTE with FP16 DHW_OPERATING_MODE_AWAY_ON, or DHW_OPERATING_MODE_OFF to end.
779 ///
780 /// See set_dhw_operating_mode_() for the sequence. Ending away also ends a boost (ADR 0081). Gated on
781 /// device_feature::WATER_HEATER.
782 /// \todo Experimental: confirm on a real water heater that this starts and ends the holiday mode.
783 /// @param device_id Target device ID (hex string).
784 /// @param on true to start away, false to end it.
785 /// @return true when the device accepted the EXECUTE; false for a refused or unacknowledged command.
786 virtual bool set_water_heater_away(const std::string &device_id, bool on);
787 /// @brief Set the temperature encoding range of a heater that carries a temperature in its main
788 /// parameter (YAML `min_temperature:` / `max_temperature:`).
789 /// @param device_id Target device ID.
790 /// @param min_c Lower end of the range in degrees Celsius.
791 /// @param max_c Upper end; must exceed `min_c`.
792 virtual void set_device_temperature_range(const std::string &device_id, float min_c, float max_c);
793 /// @brief Read one register of a heater and fold the decoded value into IoDevice::heating.
794 ///
795 /// Same gate as send_heating_command(); the read itself is read_private_register_(). A reply that
796 /// decodes (decode_heating_reply()) updates the device record and notifies the entity callbacks,
797 /// so what an entity shows is what the heater reported.
798 /// @param device_id Target device ID (hex string).
799 /// @param fn Register to read; must be readable (see heating_function_is_readable()).
800 /// @param[out] value_out Decoded value (NaN for a room-temperature sensor fault); pass nullptr when
801 /// only the stored reading is wanted.
802 /// @param max_tries Transmit-attempt cap: EXCHANGE_RETRY_COUNT for a read the user waits on (the
803 /// action, the read-back of a write), the scheduled-poll band for the background poll.
804 /// @return true only if the device answered and the reply decoded to a value for `fn`.
805 virtual bool read_heating_register(const std::string &device_id, HeatingFunction fn, float *value_out,
806 uint8_t max_tries);
807 /// @brief Queue an async position update; returns immediately, executed in loop().
808 ///
809 /// If a pending SET_TILT operation for the same device is already in the queue, the two are
810 /// coalesced into a single SET_POSITION_AND_TILT command to avoid two radio exchanges.
811 /// This transparently handles Home Assistant sending cover.set_cover_position and
812 /// cover.set_cover_tilt_position as separate rapid calls.
813 /// @param device_id Target device ID.
814 /// @param position Desired position (0–100).
815 virtual void queue_set_device_position(const std::string &device_id, uint8_t position);
816 /// @brief Queue an async named command (STOP, FAVORITE, VENT, PARTIAL, PEDESTRIAN, FORCE_OPEN);
817 /// returns immediately, executed in loop(). PEDESTRIAN is experimental.
818 ///
819 /// Existing entity/button callers (cover, favorite, vent, partial and pedestrian buttons) intentionally ignore
820 /// the return value — they always target a known, already-registered device. It exists so
821 /// force_open_device() can report enqueue rejection distinctly from a queued-but-not-yet-run
822 /// command.
823 /// @param device_id Target device ID.
824 /// @param cmd Named command to send.
825 /// @return true if the hub is initialized, the device is registered, and the command matches
826 /// its capability class (so the command was enqueued); false otherwise.
827 virtual bool queue_device_command(const std::string &device_id, CoverCommand cmd);
828 /// @brief Queue an async tilt update; returns immediately, executed in loop().
829 ///
830 /// If a pending SET_POSITION operation for the same device is already in the queue, the two are
831 /// coalesced into a single SET_POSITION_AND_TILT command to avoid two radio exchanges.
832 /// This transparently handles Home Assistant sending cover.set_cover_position and
833 /// cover.set_cover_tilt_position as separate rapid calls.
834 /// @param device_id Target device ID.
835 /// @param tilt_percent Desired tilt (0–100).
836 virtual void queue_set_device_tilt(const std::string &device_id, uint8_t tilt_percent);
837 /// Queue an async combined position+tilt update; returns immediately, executed in loop().
838 /// @param device_id Target device ID.
839 /// @param position Desired position (0–100).
840 /// @param tilt_percent Desired tilt (0–100).
841 virtual void queue_set_device_position_and_tilt(const std::string &device_id, uint8_t position, uint8_t tilt_percent);
842 /// Queue an async status request; returns immediately, executed in loop().
843 /// @param device_id Target device ID.
844 virtual void queue_request_device_status(const std::string &device_id);
845 /// Queue an async minimum-limitation read for the rain sensor poll; executed in loop() as a
846 /// background operation (yields to commands and 1W activity).
847 /// @param device_id Target device ID.
848 virtual void queue_request_device_limitation(const std::string &device_id);
849 /// Queue an async sun level poll; executed in loop() as a background operation.
850 /// @param device_id Target device ID.
851 virtual void queue_request_device_sun_level(const std::string &device_id);
852 /// Queue an async battery level poll; executed in loop() as a background operation.
853 /// @param device_id Target device ID.
854 virtual void queue_request_device_battery_level(const std::string &device_id);
855 /// Queue an async device-name request; returns immediately, executed in loop().
856 /// @param device_id Target device ID.
857 virtual void queue_request_device_name(const std::string &device_id);
858 /// Queue a pairing operation; executed in loop() when radio idle.
859 ///
860 /// A press handled within PAIRING_STALE_PRESS_MS of the end of an attempt is dropped with a log
861 /// line: pairing blocks loop(), so a press made while it ran is only handled once it ends, when the
862 /// device has usually left its pairing mode.
863 virtual void queue_discover_and_pair();
864 /// @brief Entry point for the "Scan Paired Devices" button: run the roll-call and publish its
865 /// report to the log and the Home Assistant result event, exactly as the native API action does.
866 ///
867 /// Deliberately not queued through OperationQueue, unlike queue_discover_and_pair(): the
868 /// roll-call has no priority, coalescing or dedup semantics to preserve, and it already runs
869 /// this way from the native API action. What it *is* guarded on is `busy_` — a button, unlike an
870 /// API action, is also reachable from an ESPHome automation, which can fire from inside a
871 /// blocking exchange (an entity callback -> on_value: -> button.press: chain) and would
872 /// otherwise re-enter ExchangeEngine mid-exchange. See ADR 0013 for why one blocking radio
873 /// operation at a time is the whole concurrency model. A press while `busy_` still fires the
874 /// log/event pair (a failed result), matching every other rejected management action rather
875 /// than going silent.
876 ///
877 /// Blocks the ESPHome loop for up to roughly 6 × `pairing_discovery_wait_ms` (both power-class
878 /// passes, three channels each; fewer if `scan_power_classes` narrows the sweep) and will log the
879 /// "operation took a long time" warning, same as the action — see docs/pairing.md.
881 /// Async form of set_light_position() that keeps radio work serialized on the main loop.
882 /// queue_set_light_state() is a thin binary-position wrapper around this.
883 /// @param device_id Target device ID.
884 /// @param position Desired IO position (0-100).
885 virtual void queue_set_light_position(const std::string &device_id, uint8_t position);
886 /// Async form of set_fan_position() that keeps radio work serialized on the main loop.
887 /// @param device_id Target device ID.
888 /// @param position Desired IO position (0-100).
889 virtual void queue_set_fan_position(const std::string &device_id, uint8_t position);
890 /// Async form of set_light_state() that keeps radio work serialized on the main loop.
891 /// @param device_id Target device ID.
892 /// @param on Desired on/off state.
893 virtual void queue_set_light_state(const std::string &device_id, bool on);
894 /// Async form of set_switch_state() that keeps radio work serialized on the main loop.
895 /// @param device_id Target device ID.
896 /// @param on Desired on/off state.
897 virtual void queue_set_switch_state(const std::string &device_id, bool on);
898 /// Async form of set_lock_state() that keeps radio work serialized on the main loop.
899 /// @param device_id Target device ID.
900 /// @param locked Desired locked/unlocked state.
901 virtual void queue_set_lock_state(const std::string &device_id, bool locked);
902
903#ifdef IOHOME_LR1121_FIRMWARE_UPDATE
904 /// @brief Entry point for the "Flash LR1121 Radio Firmware" button. Thin forwarder to the
905 /// Lr1121FirmwareUpdateController collaborator (lr1121_firmware_update_controller.h).
906 ///
907 /// Only exists when a `lr1121_firmware_update:` block is configured. See
908 /// lr1121_firmware_update_controller.cpp for the full contract, including the safety invariant
909 /// that every bootloader excursion this triggers must end in either radio_->init() or
910 /// App.safe_reboot() — there is no third option.
911 void trigger_lr1121_firmware_update() { this->lr1121_firmware_update_.trigger(); }
912
913#ifdef IOHOME_LR1121_BOOTLOADER_UPDATE
914 /// @brief Set by the "Allow LR1121 Bootloader Rewrite (Irreversible)" switch's write_state().
915 /// Thin forwarder to the Lr1121FirmwareUpdateController collaborator.
916 ///
917 /// A permission, not an override: this can only convert a cached
918 /// BootloaderUpgradePath::AVAILABLE verdict into "run the three-stage sequence" (bootloader
919 /// ADR 0021) -- it never affects REJECT_WRONG_CHIP, the post-entry sanity
920 /// check, the busy_ guard, or any other verdict. Read once, at button-press time
921 /// (trigger_lr1121_firmware_update()); the ESPHome loop is blocked for the whole three-stage
922 /// sequence once it starts, so the switch cannot change mid-flash. Deliberately not named
923 /// anything with "armed" -- lr1121_flash_confirmation_armed_ already means the two-press window
924 /// this switch *replaces* for its own path, and a reader must never have to guess which is meant.
925 void set_bootloader_rewrite_allowed(bool allowed) {
926 this->lr1121_firmware_update_.set_bootloader_rewrite_allowed(allowed);
927 }
928#endif
929#endif
930
931 protected:
932 // --- Protocol-level operations ---
933 /// Transmit a raw IoFrame on the current frequency with given preamble length.
934 /// @param frame IoFrame to transmit.
935 /// @param freq RF frequency in Hz.
936 /// @param preamble Preamble length in bytes (e.g. `LONG_PREAMBLE`, `SHORT_PREAMBLE`, or a
937 /// tuning-configured value such as `normal_start_preamble`).
938 bool transmit_frame_(const IoFrame &frame, uint32_t freq, uint16_t preamble);
939 /// Main request/response exchange with retry and automatic authentication.
940 /// @param request Outbound request IoFrame.
941 /// @param response Output: received response IoFrame.
942 /// @param freq RF frequency in Hz.
943 /// @param attempts A transmit-attempt cap (forwarded to ExchangeEngine::send_and_receive()), or a
944 /// read kind whose plan the engine builds for @p request (ExchangeEngine::plan_read()).
945 /// @return true if exchange succeeded; false otherwise.
946 ExchangeOutcome send_and_receive_(const IoFrame &request, IoFrame &response, uint32_t freq,
947 const decisions::AttemptRequest &attempts = EXCHANGE_RETRY_COUNT);
948 /// Handle an inbound authenticated command from a device (status updates, etc.).
949 /// @param request Inbound authenticated request (e.g., CMD_STATUS_UPDATE).
950 /// @param freq RF frequency the packet arrived on.
951 /// @return true if authentication succeeded; false otherwise.
952 bool authenticate_request_(const IoFrame &request, uint32_t freq);
953 /// Parse a received frame, merge supported device state or metadata, and notify callbacks.
954 /// @param packet Raw radio packet containing a parsed IoFrame.
955 void process_received_packet_(const RadioRxPacket &packet);
956
957 /// Handle an authenticated CMD_STATUS_UPDATE from a registered device: a device event report
958 /// (decisions::is_device_event_report()) is recorded, any other 0x71 is applied as a position update.
959 /// @param id The device's node ID string.
960 /// @param dev The device record.
961 /// @param frame The status update.
962 void apply_status_update_(const std::string &id, IoDevice &dev, const IoFrame &frame);
963
964 /// Record @p frame as a device event when it qualifies: its source is a registered device and it is
965 /// either a device event report (decisions::is_device_event_report()), or any frame that reached
966 /// the unhandled-command fallback while `diagnostic_probes` is on.
967 /// @param frame The received frame.
968 /// @param reached_unhandled_fallback True from the fallback after every other handler declined.
969 /// @return true when the frame was recorded.
970 bool record_if_device_event_(const IoFrame &frame, bool reached_unhandled_fallback);
971
972 /// Record a device event report, or a frame from a registered device that nothing else here
973 /// handles: log it raw, count it, and publish it on the "Last Device Event" sensor. The recording
974 /// never answers the frame and never applies it to the device record, whatever it claims to carry
975 /// (ADR 0022, ADR 0060). The one acknowledgement is the ordinary one: a CMD_STATUS_UPDATE addressed
976 /// to this hub is authenticated and acknowledged with CMD_STATUS_UPDATE_RESP before it gets here,
977 /// whatever event it carries. Logs at INFO with `diagnostic_probes: true` and at DEBUG otherwise, since a
978 /// subscription outlives the build that made it and must not fill a normal build's log.
979 /// @param frame The received frame; its source is a registered device.
980 void record_device_event_(const IoFrame &frame);
981
982 /// True while the key-extraction responder is mid-attempt and still within its bounded listen
983 /// window. Thin forwarder to KeyExtractionResponder::awaiting_reply() (key_extraction_responder.h)
984 /// — kept on the hub because defer_background_poll_() and tests/hub/hub_core_test.cpp reach it here,
985 /// mirroring the two set_key_extraction_armed* bindings.
986 [[nodiscard]] bool key_extraction_awaiting_reply_() const { return this->key_extraction_.awaiting_reply(); }
987
988 /// One idle pass of listening for the key-extraction responder's peer, in place of the generic idle
989 /// hop while key_extraction_awaiting_reply_(). Inside the session window
990 /// (KeyExtractionResponder::in_session_hold()) it keeps the radio on the session's channel, where
991 /// the peer's in-session frame comes; after it, a rotating listen over all three channels for one
992 /// KEY_EXTRACTION_SCAN_SLICE_MS slice, handing a received frame to process_received_packet_().
993 /// While a discovery answer waits for its slot, the slice ends at that slot (and is skipped once
994 /// it has come), so the scheduler sends the answer on time.
996
997 /// Extract supported position or metadata info from a response frame and merge it into the device record.
998 /// @param frame IoFrame containing a supported inbound command such as CMD_PRIVATE_RESP,
999 /// CMD_STATUS_UPDATE, CMD_GET_NAME_RESP, or CMD_GET_INFO2_RESP.
1000 /// @param trust_position False to apply `is_stopped` but skip target/position decode for a
1001 /// CMD_PRIVATE_RESP — the immediate reply to our own just-sent CMD_EXECUTE echoes stale
1002 /// pre-command target/current values on at least some devices (see
1003 /// tests/corpus/captures/exchange/somfy_awning_exchange_ack_reports_stale_target_*.yaml), so
1004 /// execute_request_and_update_() passes false there; every other caller trusts as before.
1005 void update_device_status_(const IoFrame &frame, bool trust_position = true);
1006 /// The device just reported its status: move its status refresh (if it has one) a whole interval out.
1007 /// @param device_id Device that reported.
1008 void note_status_reported_(const std::string &device_id);
1009 /// Record that a 1W frame just went out on the radio — ours or someone else's — updating
1010 /// last_1w_activity_ms_ and — when this frame starts a new burst (see
1011 /// decisions::oneway_burst_started_fresh()) — first_1w_activity_ms_.
1012 ///
1013 /// Called both from process_received_packet_() for an overheard remote's frame and from
1014 /// execute_oneway_command_()/execute_oneway_position_() (hub_operations.cpp) for a frame this
1015 /// hub just transmitted itself. That second case is deliberate, not a misuse of a receive-path
1016 /// hook: our own burst should defer background polls exactly like a remote's does, because it
1017 /// puts the same 1W traffic on the same shared channel the polls would otherwise use, and the
1018 /// devices it targets need the same settling time either way.
1019 /// @param now millis() at which this frame was seen or sent.
1020 void record_1w_activity_(uint32_t now);
1021 /// If `frame` matches a 1W remote's pairing gesture (decisions::is_one_way_pairing_gesture()),
1022 /// remember it (src/dst/cmd plus the radio's last-capture RSSI) in
1023 /// recent_oneway_pairing_sighting_ so a fresh discover_and_pair() attempt can seed its telemetry
1024 /// with it — see RecentOneWayPairingSighting's doc comment (issue #27/#65). A no-op for any
1025 /// other frame. Called from process_received_packet_()'s 1W-frame path, unconditionally (before
1026 /// the burst-dedup check, like record_1w_activity_()) so a repeated gesture frame still
1027 /// refreshes the timestamp (and RSSI).
1028 /// @param frame Parsed 1W frame (CTRL0 1W bit already confirmed set by the caller).
1029 /// @param now millis() at which this frame was seen.
1030 void record_oneway_pairing_gesture_(const IoFrame &frame, uint32_t now);
1031 /// Schedule a delayed status poll for a registered device using the Component timeout API.
1032 /// @param device_id ID of the device to poll.
1033 /// @param delay_ms Delay in milliseconds before polling.
1034 /// @note Uses ESPHome's set_timeout() mechanism; the callback executes in loop().
1035 /// A zero delay schedules immediately on the next loop iteration.
1036 void schedule_status_poll_(const std::string &device_id, uint32_t delay_ms);
1037 /// Arm the heater's regular-cadence poll timer. Own timer id (decisions::heating_poll_timer_id()),
1038 /// so remote-activity polls scheduled by schedule_status_poll_() never replace it.
1039 /// @param device_id ID of the heater.
1040 /// @param delay_ms Delay in milliseconds before the poll is queued.
1041 void schedule_heating_poll_(const std::string &device_id, uint32_t delay_ms);
1042 /// Begin bounded follow-up polling for a device after a command or overheard remote activity.
1043 /// @param device_id ID of the device to poll.
1044 /// @param initial_delay_ms Delay before the first follow-up poll.
1045 void begin_status_poll_tracking_(const std::string &device_id, uint32_t initial_delay_ms);
1046 /// Settle every device whose polling window ran out (StatusPollPolicy::pop_ended_device()).
1047 /// A device still settling — its last report said "stopped" off its target and the confirming
1048 /// poll never came back — is taken as at rest there: with no poll left, the device's own flag
1049 /// stands. Any other device is left as it is.
1051 /// Arm the confirming poll that follows a command, because a CMD_EXECUTE reply is never trusted
1052 /// for position (see update_device_status_()'s trust_position parameter) and therefore leaves the
1053 /// hub with no idea where the device actually is. Re-arms the bounded tracking window rather than
1054 /// only setting a due time: the same untrusted reply clears that window whenever it claims the
1055 /// device is stopped, and pop_due_device() discards a due poll that has no active window. An
1056 /// already-scheduled earlier poll wins.
1057 /// @param device_id Device the command was sent to.
1058 /// @param for_stop True for STOP (and position POS_STOP), which settles under
1059 /// STOP_SETTLE_POLL_CAP_MS instead of the normal settle cadence.
1060 void arm_execute_confirmation_poll_(const std::string &device_id, bool for_stop);
1061 /// Schedule status polls for a fixed list of devices (shared by the id-linked and
1062 /// class-linked 1W paths).
1063 /// @param device_ids Devices to poll.
1064 /// @param delay_ms Poll delay in milliseconds.
1065 void schedule_device_polls_(const std::vector<std::string> &device_ids, uint32_t delay_ms);
1066 /// Whether loop() should skip dispatching the queue this iteration because the pending work is a
1067 /// background poll and either a 1W remote transmitted very recently, or a key-extraction attempt
1068 /// is mid-flight. Thin wrapper binding the component's state to
1069 /// decisions::defer_background_poll_for_1w_activity(), plus a second, independent yield condition:
1070 /// a background poll is a blocking exchange that owns the radio for 1-3 s, and dispatching one
1071 /// while the key-extraction responder is listening for its peer's next frame (an expected
1072 /// CMD_KEY_TRANSFER (0x32), say) would swallow it — see listen_for_key_extraction_peer_() for the
1073 /// other half of that listen. Only background polls yield here, same as the 1W rule: a user
1074 /// command must never wait on either kind of background activity.
1075 [[nodiscard]] bool defer_background_poll_() const {
1076 const bool next_op_is_background =
1078 if (next_op_is_background && this->key_extraction_awaiting_reply_())
1079 return true;
1081 this->last_1w_activity_ms_, millis(),
1082 ONEWAY_QUIET_PERIOD_MS, ONEWAY_POLL_DEFER_CAP_MS);
1083 }
1084 /// Decide, and act on, what a frame of another controller means for one registered device: a
1085 /// heard write, or a device challenge that follows no heard read, arms a tracked poll; any other
1086 /// frame arms one only if the device was not read recently (StatusPollPolicy::on_foreign_frame());
1087 /// while such a poll is pending the frame only pushes its timer out. A frame that arms nothing
1088 /// changes neither the tracking window nor the failure streaks.
1089 /// @param device_id Registered device the frame concerns.
1090 /// @param kind Request to the device, or the device's challenge to another controller.
1091 /// @param controller The other controller's node ID (request source, or challenge destination).
1092 /// @param cmd Command of the heard frame.
1093 void act_on_foreign_frame_(const std::string &device_id, ForeignFrameKind kind, const std::string &controller,
1094 uint8_t cmd);
1095 /// Hand a registered device's challenge (0x3C) to another controller to act_on_foreign_frame_().
1096 /// A challenge addressed to this hub, from an unregistered node, or a 0x3D is left alone.
1097 /// @param frame An exchange-internal frame (is_exchange_internal_command()).
1098 void note_foreign_challenge_(const IoFrame &frame);
1099 /// Hand another controller's frame to act_on_foreign_frame_(): per the registered device it is
1100 /// addressed to, or per device linked to its source when that is a linked 2W controller.
1101 /// @param frame A parsed 2W frame that is not a status reply.
1102 /// @return true when the frame was another controller's request and has been handled.
1103 bool act_on_overheard_request_(const IoFrame &frame);
1104 /// Resolve the set of devices a 1W frame should affect: devices linked to the sending remote
1105 /// by node ID, plus — when the frame targets a typed broadcast (e.g. "all awnings") — devices
1106 /// linked to that device class, deduplicated so a device linked both ways is touched once.
1107 /// @param info Already-decoded 1W frame info (see decode_1w_frame()).
1108 /// @param src_id Sender's node ID as a string (already computed by the caller).
1109 /// @return Deduplicated device IDs (may be empty).
1110 [[nodiscard]] std::vector<std::string> resolve_1w_target_devices_(const OneWayFrameInfo &info,
1111 const std::string &src_id) const;
1112 /// Apply optimistic target state to every device in @p device_ids, when the decoded frame
1113 /// carries a resolvable intent. Skips devices whose known type doesn't match the frame's
1114 /// typed-broadcast target (an "all awnings" press must not optimistically move a linked
1115 /// shutter — it is still polled by schedule_device_polls_()). No-op per device when that
1116 /// device has `optimistic_state == false` (see DeviceRegistry::apply_optimistic_target()).
1117 /// @param info Already-decoded 1W frame info (see decode_1w_frame()).
1118 /// @param device_ids Devices to apply optimistic state to (see resolve_1w_target_devices_()).
1119 /// @return true if the intent resolved to a STOP (caller should poll immediately).
1120 bool apply_optimistic_linked_state_(const OneWayFrameInfo &info, const std::vector<std::string> &device_ids);
1121 /// React to a deduplicated 1W press from a remote: apply the optimistic state to the devices it
1122 /// controls (resolve_1w_target_devices_()), schedule their status polls (at once after a STOP,
1123 /// otherwise after `remote_activity_poll_delay_ms`) and, when switched on, open the catch-up burst
1124 /// (StatusPollPolicy::on_linked_remote_activity(), ADR 0080). No-op for a remote that controls none.
1125 /// @param info Already-decoded 1W frame info (see decode_1w_frame()).
1126 /// @param src_id Sender's node ID as a string (already computed by the caller).
1127 void follow_linked_remote_press_(const OneWayFrameInfo &info, const std::string &src_id);
1128 /// Fire the sender HA event for a decoded 1W frame, if the sender is exposed.
1129 /// DEBUG-logs the reason when it does not fire (API disconnected / sender not exposed) so a
1130 /// live log capture can distinguish "never reached this check" from "reached it and skipped".
1131 /// @param info Already-decoded 1W frame info (see decode_1w_frame()).
1132 /// @param linked True if the sender is linked to at least one registered device.
1133 /// @param src_id Sender's node ID as a string (already computed by the caller).
1134 void maybe_fire_sender_event_(const OneWayFrameInfo &info, bool linked, const std::string &src_id);
1135 /// Handle an explicit CMD_ERROR_RESP refusal from the device: record the result code, stamp link
1136 /// health, and schedule the poll backoff. Split out of exchange_and_record_() to keep that
1137 /// function's outcome dispatch readable — a refusal is a distinct concern from "what did the
1138 /// exchange achieve".
1139 /// @param device_id Target device ID.
1140 /// @param request Outbound request frame that drew the refusal.
1141 /// @param response The CMD_ERROR_RESP frame.
1142 /// @param retry_after_fail_ms If non-zero, schedules next status poll after this delay.
1143 /// @return Always false; a refusal is never a success.
1144 bool handle_error_response_(const std::string &device_id, const IoFrame &request, const IoFrame &response,
1145 uint32_t retry_after_fail_ms);
1146
1147 /// Fold one rain poll outcome into the device's rain sensor state and tell subscribers.
1148 /// @param device_id Target device ID.
1149 /// @param reading DRY or RAIN for an answered poll, UNKNOWN for a miss.
1150 /// @param limit_text Rendering of the reported limit for the state-change log, empty for a miss.
1151 /// @param unanswered True when the device sent nothing usable back at all (no reply, no error),
1152 /// so the miss is retried once after PERIODIC_POLL_MISS_RETRY_MS before it counts.
1153 void apply_rain_poll_result_(const std::string &device_id, RainSensorState reading, const std::string &limit_text,
1154 bool unanswered);
1155
1156 /// @brief The two subscription-answer sensor polls (ADR 0064).
1157 enum class EventReading : uint8_t {
1158 SUN_LEVEL, ///< EVENT_LOCAL_SENSOR, IoDevice::sun_level.
1159 BATTERY_LEVEL, ///< EVENT_BATTERY_LEVEL_CHANGE, IoDevice::battery_level.
1160 };
1161
1162 /// Shared body of request_device_sun_level() and request_device_battery_level(): defer while the
1163 /// device moves, subscribe, decode the answer, fold the result into the device record.
1164 /// @param device_id Target device ID.
1165 /// @param kind Which reading to poll.
1166 /// @return true if an answer of the captured shape arrived and was applied.
1167 bool request_event_reading_(const std::string &device_id, EventReading kind);
1168
1169 /// The schedule of @p kind's poll.
1170 /// @param kind Which reading.
1171 /// @return The PeriodicPollPolicy instance that schedules it.
1173
1174 /// Fold one poll outcome into the device record: a reading replaces the value and clears the miss
1175 /// count; a miss keeps the last value until PERIODIC_POLL_FAILURES_BEFORE_UNKNOWN. Logs a change
1176 /// at INFO and tells subscribers in every case.
1177 /// @param device_id Target device ID.
1178 /// @param kind Which reading.
1179 /// @param reading The decoded value of an answered poll, empty for a miss.
1180 /// @param unanswered True when the device sent nothing back at all, so the miss is retried once
1181 /// after PERIODIC_POLL_MISS_RETRY_MS before it counts.
1182 void apply_event_reading_(const std::string &device_id, EventReading kind, const std::optional<uint16_t> &reading,
1183 bool unanswered);
1184
1185 /// Queue every periodic poll that is due: the sensor polls (rain, sun level, battery level) and the
1186 /// status refresh at rest.
1188 /// Read every device with `linked_remotes` once, when a burst of linked-remote activity has been
1189 /// followed by `remote_activity_catchup_poll_ms` without an exchange (ADR 0080). Each read is a
1190 /// one-off status read marked state-unknown (StatusPollPolicy::mark_state_unknown()); it opens no
1191 /// follow-up window of its own, an answer that says moving does.
1193
1194 /// @brief What exchange_and_record_() found.
1195 enum class RequestResult : uint8_t {
1196 FAILED, ///< No usable reply (silence, or an unconfirmed acceptance of a request that needs a payload).
1197 UNCONFIRMED, ///< A CMD_EXECUTE the device accepted but never closed; counted as success, no reply frame.
1198 ERROR_REPLY, ///< The device refused with CMD_ERROR_RESP; handle_error_response_() has recorded it.
1199 REPLY, ///< The device answered; the caller interprets the reply frame.
1200 };
1201
1202 /// @brief Per-request knobs of exchange_and_record_().
1204 bool warn_on_no_response; ///< Log a warning when no response arrives.
1205 uint32_t retry_after_fail_ms; ///< If non-zero, drives the status-poll backoff and failure streaks.
1206 decisions::AttemptRequest attempts; ///< Attempt cap or read kind forwarded to send_and_receive_().
1207 };
1208
1209 /// send_and_receive_() on the command channel, except that a device event report
1210 /// (decisions::is_device_event_report()) taken as the reply is recorded (record_device_event_())
1211 /// and the request asked once more; a second report in a row, or one answering an EXECUTE that is
1212 /// not safe to repeat, ends it as FAILED.
1213 /// @param request Outbound request frame.
1214 /// @param response Filled with the reply on SUCCESS_WITH_RESPONSE; never a device event report then.
1215 /// @param attempts Attempt budget per send, as for send_and_receive_().
1216 /// @return The exchange outcome.
1218 const decisions::AttemptRequest &attempts);
1219
1220 /// Run one authenticated request/response exchange on the standard command channel and record
1221 /// its outcome: exchange-outcome and timeout counters, link health, challenge-seen handling,
1222 /// CMD_ERROR_RESP handling, debug logs and (when requested) the status-poll backoff. It does not
1223 /// interpret a reply — callers decide what a REPLY means.
1224 /// @param device_id Target device ID.
1225 /// @param request Outbound request frame.
1226 /// @param[out] response The device's reply, valid for RequestResult::REPLY and ERROR_REPLY.
1227 /// @param options See RequestOptions.
1228 /// @return What happened, see RequestResult.
1229 RequestResult exchange_and_record_(const std::string &device_id, const IoFrame &request, IoFrame &response,
1230 const RequestOptions &options);
1231
1232 /// Shared request/response helper for high-level operations: exchange_and_record_() plus
1233 /// update_device_status_() on a reply.
1234 /// @param device_id Target device ID.
1235 /// @param request Outbound request frame.
1236 /// @param warn_on_no_response If true, logs a warning when no response is received.
1237 /// @param retry_after_fail_ms If non-zero, schedules next status poll after this delay on failure.
1238 /// @param attempts Attempt cap or read kind, forwarded to send_and_receive_(). Defaults to the full
1239 /// EXCHANGE_RETRY_COUNT; a cover's status poll passes its read kind
1240 /// (decisions::scheduled_poll_read_kind()).
1241 /// @return true when the device replied, or when a CMD_EXECUTE was accepted without a reply —
1242 /// every other command's unconfirmed acceptance is still a failure here (see
1243 /// @ref ExchangeOutcome and decisions::retry_after_unconfirmed_accept_is_safe()).
1244 bool execute_request_and_update_(const std::string &device_id, const IoFrame &request, bool warn_on_no_response,
1245 uint32_t retry_after_fail_ms = 0,
1246 const decisions::AttemptRequest &attempts = EXCHANGE_RETRY_COUNT);
1247
1248 /// @brief Everything one execute-family operation needs beyond its own guard and frame builder.
1250 const char *action; ///< Verb/phrase for the "Sending ..." and rejection logs.
1251 bool settle_as_stop; ///< Passed through to arm_execute_confirmation_poll_().
1252 };
1253 /// Funnel for the four execute-family operations: runs try_execute_operation_() and, on any
1254 /// false return (the command will not reach the device), withdraws the optimistic prediction the
1255 /// entity applied at control() time via DeviceRegistry::rollback_optimistic(). Wrapping rather
1256 /// than inlining the rollback keeps every current and future failure exit covered by one call.
1257 /// @param device_id Target device ID.
1258 /// @param spec Pre-formatted action phrase and the settle-as-stop flag.
1259 /// @param accepts Capability guard; returns false to reject the operation for this device.
1260 /// @param rejection_profile Expected-profile label for the rejection log.
1261 /// @param build Fills the request frame from the resolved device; returns false on failure.
1262 /// @return true when the exchange succeeded and the settle poll was armed.
1263 bool run_execute_operation_(const std::string &device_id, const ExecuteRequestSpec &spec,
1264 const std::function<bool(const IoDevice &)> &accepts, const char *rejection_profile,
1265 const std::function<bool(IoFrame &, const IoDevice &)> &build);
1266
1267 /// Shared skeleton for the four execute-family operations (position, named command, tilt,
1268 /// position+tilt): device lookup + initialized guard, capability guard, poll-tracking start,
1269 /// "Sending ..." log, frame build, exchange, failure backoff, and the settle poll. The four
1270 /// public methods supply only their guard predicate, rejection profile label, and frame
1271 /// builder. Called only through run_execute_operation_(), which owns the failure rollback.
1272 /// @param device_id Target device ID.
1273 /// @param spec Pre-formatted action phrase and the settle-as-stop flag.
1274 /// @param accepts Capability guard; returns false to reject the operation for this device.
1275 /// @param rejection_profile Expected-profile label for the rejection log.
1276 /// @param build Fills the request frame from the resolved device; returns false on failure.
1277 /// @return true when the exchange succeeded and the settle poll was armed.
1278 bool try_execute_operation_(const std::string &device_id, const ExecuteRequestSpec &spec,
1279 const std::function<bool(const IoDevice &)> &accepts, const char *rejection_profile,
1280 const std::function<bool(IoFrame &, const IoDevice &)> &build);
1281
1282 /// Attempts for a cover's status read: its read kind (a settling read or a look at a busy device,
1283 /// decisions::scheduled_poll_read_kind()), consuming the poll's STOP and confirming-read marks.
1284 /// @param device_id Device being polled.
1285 /// @param scheduler_managed True when StatusPollPolicy is tracking the device (a ladder stands behind the poll).
1286 /// @param state_unknown True for a catch-up read (StatusPollPolicy::take_state_unknown()).
1287 [[nodiscard]] decisions::AttemptRequest cover_poll_attempts_(const std::string &device_id, bool scheduler_managed,
1288 bool state_unknown = false);
1289 /// Attempts for a heater's status read: the count-based budget heaters keep
1290 /// (decisions::scheduled_poll_max_tries()).
1291 /// @param device_id Device being polled.
1292 /// @param scheduler_managed True when StatusPollPolicy is tracking the device.
1293 [[nodiscard]] decisions::AttemptRequest heater_poll_attempts_(const std::string &device_id, bool scheduler_managed);
1294
1295 /// Execute a named device command (STOP, FAVORITE, VENT, PARTIAL, PEDESTRIAN, FORCE_OPEN) via the
1296 /// authenticated exchange.
1297 /// A named position (NAMED_POSITIONS) goes out only to a device whose features include its bit.
1298 /// @param device_id Target device ID.
1299 /// @param cmd Named command to execute.
1300 /// @return true if device acknowledged; false otherwise, including a named position the device's
1301 /// features lack (nothing is sent).
1302 bool execute_device_command_(const std::string &device_id, CoverCommand cmd);
1303 /// Shared bookkeeping for every 1W transmit: mark the radio busy for the duration of `send`,
1304 /// then record it as 1W activity so background polls back off for it exactly as they do for a
1305 /// remote's burst — the radio is equally busy either way. Every 1W execute must go through this;
1306 /// a future one that skips it would compile, pass, and silently break poll-deferral.
1307 /// @param send Callable that performs the actual transmit; takes no arguments.
1308 template<typename F> void execute_oneway_(F &&send) {
1309 this->busy_ = true;
1310 send();
1311 this->busy_ = false;
1312 this->record_1w_activity_(millis());
1313 }
1314 /// Send a queued 1W named command. Unlike its 2W sibling this returns nothing: there is no
1315 /// acknowledgement to report, and success here would only mean "bytes left the radio".
1316 /// @param controller_id Controller-identity handle.
1317 /// @param cmd Named command to send.
1318 void execute_oneway_command_(const std::string &controller_id, CoverCommand cmd);
1319 /// Send a queued 1W numeric position. See execute_oneway_command_().
1320 /// @param controller_id Controller-identity handle.
1321 /// @param position Target position 0–100.
1322 void execute_oneway_position_(const std::string &controller_id, uint8_t position);
1323 /// Send a queued 1W enrollment (add-controller). See execute_oneway_command_().
1324 /// @param controller_id Controller-identity handle.
1325 void execute_oneway_enroll_(const std::string &controller_id);
1326 /// Send a queued 1W un-enrollment (remove-controller). See execute_oneway_command_().
1327 /// @param controller_id Controller-identity handle.
1328 void execute_oneway_unenroll_(const std::string &controller_id);
1329 /// Fire all registered device update callbacks for the given device ID.
1330 /// @param id Device ID that updated.
1331 void notify_device_update_(const std::string &id);
1332 /// Apply backoff after a failed background status poll and log the result.
1333 /// @param device_id Target device ID.
1334 /// @param auth_like True when the failed exchange saw a 0x3C challenge.
1335 /// @param device_moving True when the failed request was a single-try status poll of a device
1336 /// the hub believes is travelling; its first misses keep the normal cadence
1337 /// (StatusPollPolicy::on_exchange_failed()).
1338 void schedule_background_poll_backoff_(const std::string &device_id, bool auth_like, bool device_moving = false);
1339 /// @brief The exchange every heating frame shares: send on the command channel, record the
1340 /// outcome (exchange counters, link health, error result code), notify the entities and report
1341 /// whether the device answered with CMD_WRITE_PRIVATE_ACK.
1342 /// @param device_id Target device ID.
1343 /// @param label What was sent, for the log ("set_temperature", "read room_temperature").
1344 /// @param request The framed CMD_WRITE_PRIVATE request.
1345 /// @param[out] response The device's reply; valid when true is returned.
1346 /// @param max_tries Transmit-attempt cap forwarded to send_and_receive_().
1347 /// @return true only if the reply is a CMD_WRITE_PRIVATE_ACK.
1348 bool exchange_heating_(const std::string &device_id, const char *label, const IoFrame &request, IoFrame &response,
1349 uint8_t max_tries);
1350 /// @brief Read one private register raw: build the read request, run exchange_heating_() and check that
1351 /// the reply echoes the register.
1352 ///
1353 /// The one place a register read is framed and sent; read_heating_register() decodes on top of it, and
1354 /// the reads no HeatingFunction describes (a model name in ASCII, "is there a value at all") use it
1355 /// directly. Gated by device_accepts_register_probe(); the request is built by
1356 /// encode_heating_register_read(), which cannot produce a write.
1357 /// @param device_id Target device ID.
1358 /// @param reg_hi High byte of the register.
1359 /// @param reg_lo Low byte of the register.
1360 /// @param label What is read, for the log ("model_name").
1361 /// @param[out] response The reply; its value bytes start at HEATING_REPLY_VALUE_OFFSET (none for a
1362 /// register the device does not hold).
1363 /// @param max_tries Transmit-attempt cap forwarded to exchange_heating_().
1364 /// @return true when the device answered with the register's echo.
1365 bool read_private_register_(const std::string &device_id, uint8_t reg_hi, uint8_t reg_lo, const char *label,
1366 IoFrame &response, uint8_t max_tries);
1367 /// @brief The one-off model check of a heater kind that has model rows (device_offers_models()).
1368 ///
1369 /// Reads the model name (register 0x0119) and logs it at INFO, and compares whether the towel-dryer mode
1370 /// register (0x0136) holds a value with the declared `io_model:` (decisions::classify_heater_model_hint()),
1371 /// logging a WARN when they disagree. Nothing it finds changes behaviour. Done once both reads were
1372 /// answered, or after decisions::HEATER_MODEL_CHECK_MAX_RUNS runs.
1373 /// @param device_id Target device ID.
1374 /// @param max_tries Transmit-attempt cap of each read (the poll's).
1375 void check_heater_model_(const std::string &device_id, uint8_t max_tries);
1376 /// @brief Read the temperature in force of a heater's programme, when the programme runs.
1377 /// @details Sends nothing unless programme_period_active() holds for the readings the poll just made.
1378 /// When the temperature fits neither period it forgets the eco setback, so the boot registers
1379 /// read it again.
1380 /// @param device_id Target device ID.
1381 /// @param profile The device's profile; its programme_period must not be nullptr.
1382 /// @param max_tries Transmit-attempt cap of the read (the poll's).
1383 void poll_programme_period_(const std::string &device_id, const HeatingProfile &profile, uint8_t max_tries);
1384 /// @brief Poll a heater: read its profile's poll registers (stopping at the first failure), then the
1385 /// programme's temperature in force (poll_programme_period_()), then its
1386 /// boot registers until each has been read once, then, on a heater kind with model rows, the one-off
1387 /// model check (check_heater_model_()) once the device has answered a read.
1388 ///
1389 /// A failure moves the retry onto the StatusPollPolicy ladder; either way the next regular poll
1390 /// is armed one interval out (the device's `status_poll_interval`, else
1391 /// HEATING_POLL_DEFAULT_INTERVAL_MS). Called from request_device_status() for climate devices.
1392 /// @param device_id Target device ID.
1393 /// @return true when all four registers were read.
1394 bool poll_heating_device_(const std::string &device_id);
1395 /// @brief Read one family-0x00 register of a water heater raw (read_private_register_() gated on
1396 /// device_feature::WATER_HEATER).
1397 /// @param device_id Target device ID.
1398 /// @param index Low byte of the register.
1399 /// @param label What is read, for the log ("dhw_water_temperature").
1400 /// @param[out] response The reply; its value bytes start at HEATING_REPLY_VALUE_OFFSET.
1401 /// @param max_tries Transmit-attempt cap forwarded to read_private_register_().
1402 /// @return true when the device answered with the register's echo.
1403 bool read_dhw_register_(const std::string &device_id, uint8_t index, const char *label, IoFrame &response,
1404 uint8_t max_tries);
1405 /// @brief Read one family-0x00 register of a water heater and fold its value into IoDevice::dhw.
1406 /// @details A value the decoder does not take (an empty value, a mode byte never seen) is logged at
1407 /// DEBUG and leaves the reading as it was; it is still an answer, not a failed read.
1408 /// @param device_id Target device ID.
1409 /// @param index One of the DHW_REG_* registers.
1410 /// @param max_tries Transmit-attempt cap.
1411 /// @return true when the device answered.
1412 bool read_dhw_reading_(const std::string &device_id, uint8_t index, uint8_t max_tries);
1413 /// @brief Read a water heater's target, capabilities and boost and away state: the status read
1414 /// DHW_STATUS_FPI1 / DHW_STATUS_FPI2 (create_status_mp_fp_read()), whose reply update_device_status_()
1415 /// folds into IoDevice::dhw.
1416 /// @param device_id Target device ID.
1417 /// @param max_tries Transmit-attempt cap.
1418 /// @return true when the device answered.
1419 bool read_dhw_status_(const std::string &device_id, uint8_t max_tries);
1420 /// @brief Write one family-0x00 register of a water heater, then read it back.
1421 ///
1422 /// Shared by the mode setter and the boost and away duration writes, each of which encodes its own payload. Gated on
1423 /// device_feature::WATER_HEATER; the write has the full retry budget, the read-back (read_dhw_reading_())
1424 /// updates IoDevice::dhw, and the next poll moves to HEATING_POST_WRITE_POLL_DELAY_MS from now.
1425 /// @param device_id Target device ID.
1426 /// @param operation What is written, for the log ("hot water mode").
1427 /// @param index The register written, which is also the one read back.
1428 /// @param payload The encoded write (`0C 61 00 <index> <value>`).
1429 /// @param payload_len Its length.
1430 /// @return true only if the device acknowledged the write; a missed read-back does not change that.
1431 bool write_dhw_register_(const std::string &device_id, const char *operation, uint8_t index, const uint8_t *payload,
1432 size_t payload_len);
1433 /// @brief Switch a water heater's boost or away through the operating-mode word (ADR 0081).
1434 ///
1435 /// Refused and logged when FP15 has been read and lacks the feature's capability bit. On: the duration
1436 /// register write (its ack is logged, a missed one does not stop the sequence), then one EXECUTE with
1437 /// MAIN_PARAMETER_KEEP and the feature's FP16. Off: the EXECUTE with DHW_OPERATING_MODE_OFF only. After
1438 /// an accepted EXECUTE the status is read at once and the next poll moves to HEATING_POST_WRITE_POLL_DELAY_MS
1439 /// from now.
1440 /// @param device_id Target device ID.
1441 /// @param which Boost or away.
1442 /// @param on true to start it, false to end it (which ends the other too).
1443 /// @return true when the device accepted the EXECUTE.
1444 bool set_dhw_operating_mode_(const std::string &device_id, DhwOperatingFeature which, bool on);
1445 /// @brief The water heater `device_id` names, or nullptr when it is unknown, the hub is not set up yet, or
1446 /// the device is no water heater (that case is logged). The gate of every water heater write.
1447 /// @param device_id Target device ID.
1448 /// @param operation What is being written, for the rejection log line.
1449 /// @return The device record, or nullptr.
1450 const IoDevice *water_heater_for_write_(const std::string &device_id, const char *operation);
1451 /// @brief Send one water heater EXECUTE (target or operating mode) through try_execute_operation_(), which
1452 /// gates on device_feature::WATER_HEATER and arms the settle poll; on acceptance the next poll moves to
1453 /// HEATING_POST_WRITE_POLL_DELAY_MS from now so the heating state follows.
1454 /// @param device_id Target device ID.
1455 /// @param action Operation name for the logs.
1456 /// @param build Builds the EXECUTE for the device.
1457 /// @return true when the device accepted the EXECUTE.
1458 bool execute_dhw_(const std::string &device_id, const char *action,
1459 const std::function<bool(IoFrame &, const IoDevice &)> &build);
1460 /// @brief Poll a water heater: the status read for its target, capabilities and boost and away state
1461 /// (read_dhw_status_()), then its water temperature, mode, heating state, boost and away duration and
1462 /// energy registers (stopping at the first failure), then its brand until it has been read once (logged at INFO).
1463 ///
1464 /// The same ladder, try band and cadence as poll_heating_device_(): a failure moves the retry onto the
1465 /// StatusPollPolicy ladder, and the next regular poll is armed one interval out. Called from
1466 /// request_device_status() for a device with device_feature::WATER_HEATER.
1467 /// @param device_id Target device ID.
1468 /// @return true when the status read and every register were answered.
1469 bool poll_dhw_device_(const std::string &device_id);
1470 /// Pop next pending operation from the queue and execute it (set position, request status, discover).
1472
1473 // --- Exchange helpers (thin wrappers delegating to ExchangeEngine) ---
1474
1475 /// Log the last exchange debug snapshot (delegates to exchange_engine_).
1476 void log_exchange_debug_(const char *device_id) const { this->exchange_engine_.log_debug(device_id); }
1477
1478 /// Log the last exchange debug snapshot for an accepted-but-unconfirmed exchange, at INFO.
1479 void log_exchange_unconfirmed_debug_(const char *device_id) const {
1480 this->exchange_engine_.log_debug_unconfirmed(device_id);
1481 }
1482
1483 // --- Tuning ---
1484 /// Apply the current tuning configuration to the active radio driver.
1486 /// @brief Resolve `normal_start_preamble` from the driver when YAML did not set it (ADR 0042).
1488
1489 // --- Management actions (thin wrappers delegating to management_actions_) ---
1490 /// Register hub-level Home Assistant actions; called from setup().
1492 /// Native API callback: rename a registered device.
1493 void api_rename_device_(const std::string &device_id, const std::string &new_name) {
1494 this->management_actions_.api_rename_device(device_id, new_name);
1495 }
1496 /// Native API callback: trigger a registered device's physical identify.
1497 void api_identify_device_(const std::string &device_id) { this->management_actions_.api_identify_device(device_id); }
1498 /// Native API callback: force-open a registered cover device.
1499 void api_force_open_device_(const std::string &device_id) {
1501 }
1502 /// Native API callback: broadcast a roll-call scan of already-paired devices.
1504 /// Native API callback: queue a 1W position for a controller identity.
1505 void api_oneway_set_position_(const std::string &controller_id, const std::string &position) {
1506 this->management_actions_.api_oneway_set_position(controller_id, position);
1507 }
1508 /// Native API callback: queue a 1W un-enrollment (remove-controller) for a controller identity.
1509 void api_oneway_remove_controller_(const std::string &controller_id) {
1511 }
1512 /// Native API callback: run a single diagnostic probe against a registered device.
1513 void api_probe_device_(const std::string &device_id, const std::string &probe, const std::string &index) {
1514 this->management_actions_.api_probe_device(device_id, probe, index);
1515 }
1516 /// Native API callback: run a bounded diagnostic probe sweep against a registered device.
1517 void api_probe_sweep_(const std::string &device_id, const std::string &probe, const std::string &first_index,
1518 const std::string &last_index) {
1519 this->management_actions_.api_probe_sweep(device_id, probe, first_index, last_index);
1520 }
1521 /// Native API callback: run a heating/climate function (CMD_WRITE_PRIVATE 0x20) against a
1522 /// registered climate device.
1523 void api_heating_control_(const std::string &device_id, const std::string &function, const std::string &value) {
1524 this->management_actions_.api_heating_control(device_id, function, value);
1525 }
1526
1527 // --- Frequency hopping ---
1528 void hop_frequency_();
1529
1530 // --- Radio driver selection (called once from setup()) ---
1531 /// Select and construct the radio driver named by the required `radio_type` config field.
1532 ///
1533 /// Kept as its own method rather than inlined into setup(): the three-way chip branch
1534 /// (SX1276/SX1262/LR1121) is enough logic on its own that folding it into setup() pushes
1535 /// that function's cognitive complexity past clang-tidy's threshold.
1536 /// Validates the pins each driver needs and logs a clear error (without calling
1537 /// mark_failed() itself — the caller decides how to react) when a required pin is
1538 /// missing. On success, `*chip_name_out` is set to a static string naming the selected
1539 /// chip (used for logging), and the returned pointer is the heap-allocated (not yet
1540 /// initialized) driver instance.
1541 /// @param chip_name_out Output: human-readable chip name for logging (always set,
1542 /// even on failure, to the best-known name for error messages).
1543 /// @return Newly allocated RadioDriver, or nullptr if pin validation or allocation failed.
1544 RadioDriver *select_and_construct_radio_(const char **chip_name_out);
1545
1546 /// @brief Emit the 1W controller identities to the config dump — node, class, and the resolved
1547 /// ACEI / broadcast (ADR 0031). Factored out of dump_config() to keep its cognitive complexity
1548 /// under the clang-tidy threshold.
1550
1551#ifdef IOHOME_LR1121_FIRMWARE_UPDATE
1552 // --- LR1121 firmware update: thin forwarders to the Lr1121FirmwareUpdateController collaborator
1553 // (lr1121_firmware_update_controller.h). setup()/dump_config() keep calling these names; the
1554 // orchestration and every safety invariant live in the collaborator's .cpp. ---
1555 /// Boot-time bootloader-version excursion — forwards. Called from setup() after
1556 /// select_and_construct_radio_() and before radio_->init().
1557 void run_lr1121_boot_time_bootloader_read_() { this->lr1121_firmware_update_.run_boot_time_bootloader_read(); }
1558 /// Compute and cache the flash verdict once radio_->init() has produced (or failed to produce)
1559 /// an installed-firmware-version read — forwards. Called from setup() regardless of whether
1560 /// init() succeeded.
1561 void cache_lr1121_flash_verdict_() { this->lr1121_firmware_update_.cache_flash_verdict(); }
1562 /// Emit the bootloader version and cached flash verdict to the config dump — forwards. Called
1563 /// from dump_config(), next to the existing radio_->dump_debug() call.
1564 void dump_lr1121_firmware_update_debug_() const { this->lr1121_firmware_update_.dump_debug(); }
1565#endif
1566
1567 // --- Radio driver ---
1569
1570 // --- Hardware pins (set by YAML codegen, passed to radio driver in setup) ---
1571 InternalGPIOPin *rst_pin_{nullptr};
1572 InternalGPIOPin *dio0_pin_{nullptr}; ///< SX1276 DIO0 interrupt
1573 InternalGPIOPin *dio4_pin_{nullptr}; ///< SX1276 DIO4 preamble detect (optional)
1574 InternalGPIOPin *dio1_pin_{nullptr}; ///< SX1262 DIO1 interrupt; also carries the LR1121's DIO9 IRQ line
1575 InternalGPIOPin *busy_pin_{nullptr}; ///< SX1262/LR1121 BUSY pin
1576 InternalGPIOPin *fem_en_pin_{nullptr}; ///< Front-end module enable
1577 InternalGPIOPin *vfem_pin_{nullptr}; ///< Front-end module power
1578 InternalGPIOPin *fem_pa_pin_{nullptr}; ///< Front-end module PA switch
1579 FemProfile fem_profile_{FemProfile::NONE}; ///< Which FEM part fem_pa_pin_ belongs to, if any.
1580
1581 // --- Configuration (from YAML) ---
1582 std::string node_id_str_;
1583 std::string system_key_str_;
1584 std::string radio_type_; ///< "sx1276", "sx1262", or "lr1121"; required by the YAML schema.
1585 /// Why setup() gave up on the radio, or empty. Printed by dump_config so log clients that connect
1586 /// after boot see the cause, not only ESPHome's generic "marked FAILED" line.
1588 /// Set once loop() has reported a radio that latched a failure after setup, so the report (an
1589 /// error line, ESPHome's warning status and a republish of every device) is made once, not on
1590 /// every loop.
1592 uint8_t node_id_[NODE_ID_SIZE]{};
1593 uint8_t system_key_[AES_KEY_SIZE]{};
1596 uint8_t tcxo_voltage_{DEFAULT_TCXO_VOLTAGE_SETTING_1P8V}; ///< SX1262/LR1121 TCXO voltage setting (default 1.8 V)
1597
1598 // --- Runtime state ---
1599 bool initialized_{false};
1600 bool busy_{false};
1601 /// millis() when the last pairing attempt ended; 0 before the first one (see queue_discover_and_pair()).
1603 bool radio_test_mode_{false}; ///< When true, loop() is suspended for loopback testing.
1604 TuningConfig tuning_{}; ///< Runtime tuning overrides.
1606 /// 1W sender node IDs (remotes or sensors) allowed to fire the sender HA event
1607 /// (`add_exposed_sender`). Config-time list (populated once from YAML), not a per-frame allocation.
1608 std::vector<std::string> exposed_senders_;
1609 /// Invoked once after every pairing attempt completes; see set_pairing_result_callback().
1610 std::function<void()> pairing_result_callback_;
1611 /// Device events recorded since boot, the last one's text and its subscriber; see record_device_event_().
1614 std::function<void()> device_event_callback_;
1615 /// Whether diagnostic probes (DiagnosticProbes::probe_device()/probe_sweep()) are enabled.
1616 /// False by default so a build that didn't opt in via `diagnostic_probes: true` never sends an
1617 /// undecoded probe opcode. See set_diagnostic_probes_enabled().
1620 PeriodicPollPolicy rain_poll_policy_; ///< Schedule of the opt-in rain sensor poll.
1621 PeriodicPollPolicy sun_poll_policy_; ///< Schedule of the opt-in sun level poll.
1622 PeriodicPollPolicy battery_level_poll_policy_; ///< Schedule of the opt-in battery level poll.
1623 PeriodicPollPolicy status_refresh_policy_; ///< Schedule of the opt-in status refresh at rest.
1625 /// Per-attempt pairing telemetry. PairingEngine records into it and, during an attempt, attaches it
1626 /// to ExchangeEngine as its TransmitObserver.
1628 /// Most recent 1W pairing-gesture frame seen on the hub's normal passive RX path (e.g. a PROG
1629 /// press's WRITE_PRIVATE/1W-remove/discover-alt broadcast), remembered so PairingEngine can seed
1630 /// a fresh discover_and_pair() attempt's telemetry with it — see record_oneway_pairing_gesture_()
1631 /// and RecentOneWayPairingSighting's doc comment (issue #27/#65). Declared before pairing_engine_,
1632 /// which holds a reference to it, so member-init order matches the initializer list.
1634 ExchangeEngine exchange_engine_; ///< Owns all authenticated exchange and LBT/hop logic.
1635 PairingEngine pairing_engine_; ///< Owns the three-phase device pairing flow.
1636 ManagementActions management_actions_; ///< Owns rename, identify, force-open, scan_paired_devices, and other
1637 ///< hub-level HA actions.
1638 /// Owns the 1W controller identities, their rolling-sequence counters and the transmit burst.
1639 /// The third collaborator that drives the radio (ADR 0004), and the only one that awaits
1640 /// nothing — 1W has no reply to wait for.
1642 /// Opt-in, receive-only 1W controller-key adoption listener (oneway_key_adoption.cpp). Armed via
1643 /// the "Recover 1W Controller Key" switch; observes an overheard CMD_ONEWAY_ADD_CONTROLLER and
1644 /// reports the key once, then disarms. Declared after oneway_transmitter_ so member-init order
1645 /// matches the initializer list.
1647 /// Device-role responder for the "Recover System Key" feature (key_extraction_responder.cpp).
1648 /// Owns the pairing_responder::ResponderContext, the throwaway-ID/auto-off/grace-window
1649 /// machinery, and the device-role reply TX. Declared after registry_/radio_/tuning_/node_id_
1650 /// (which it references) and after oneway_key_adoption_ so member-init order matches the
1651 /// initializer list.
1653#ifdef IOHOME_LR1121_FIRMWARE_UPDATE
1654 /// Orchestrates the compile-gated LR1121 transceiver-firmware-update feature
1655 /// (lr1121_firmware_update_controller.cpp): boot-time bootloader read, cached flash verdict,
1656 /// two-press confirmation window, and the button-triggered flash/bootloader-rewrite sequences.
1657 /// Guarded member AND guarded initializer — an unguarded initializer for a guarded member is the
1658 /// classic way this breaks only under make firmware-test.
1659 Lr1121FirmwareUpdateController lr1121_firmware_update_;
1660#endif
1661 /// Subscribers to the per-command 1W report; one per "Last 1W Command" sensor.
1662 std::vector<OneWayCommandReportFn> oneway_report_callbacks_;
1663
1664 /// Identity of the last processed 1W frame, for burst suppression; see
1665 /// decisions::is_duplicate_1w_frame() for why the intent bytes are part of the key.
1667 /// millis() of the most recent 1W frame of any kind, including ones dropped as duplicates —
1668 /// a repeat still means the remote is transmitting. 0 until the first is seen. Gates background
1669 /// polls in loop(); see decisions::defer_background_poll_for_1w_activity().
1671 /// millis() of the first 1W frame in the current burst. Advances to the new frame's timestamp
1672 /// whenever the gap since last_1w_activity_ms_ reaches ONEWAY_QUIET_PERIOD_MS (the previous burst
1673 /// has already released any deferred poll, so this one starts fresh); otherwise holds at the
1674 /// burst's start. Bounds defer_background_poll_() via ONEWAY_POLL_DEFER_CAP_MS.
1676};
1677
1678// ----------------------------------------------------------------------------
1679// Test-visible helpers (inline for host unit tests)
1680// ----------------------------------------------------------------------------
1681
1682/// Format a position float as a human‑readable string (e.g. "50%", "unknown").
1683/// @param pos Position value (0–100 or UNKNOWN_POSITION).
1684/// @return String like "50%" or "unknown".
1685inline std::string format_position(float pos) {
1686 if (pos == UNKNOWN_POSITION) {
1687 return "unknown";
1688 }
1689 char buf[POSITION_TEXT_BUFFER_SIZE];
1690 snprintf(buf, sizeof(buf), "%.0f%%", pos);
1691 return buf;
1692}
1693
1694} // namespace home_io_control
1695} // 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 set_wake_evidence_spent_handler(WakeEvidenceSpentHandler handler)
Install what to do when an exchange planned on an AWAKE belief drew no frame at all: the receiver did...
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_device_key_provider(DeviceKeyProvider provider)
Install the source of per-device system keys.
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:1576
InternalGPIOPin * fem_pa_pin_
Front-end module PA switch.
Definition hub_core.h:1578
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:350
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:627
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:637
virtual bool set_fan_position(const std::string &device_id, uint8_t position)
Send a heat level to an exterior heater (a fan entity) as an IO position.
void schedule_background_poll_backoff_(const std::string &device_id, bool auth_like, bool device_moving=false)
Apply backoff after a failed background status poll and log the result.
Definition hub_core.cpp:378
EventReading
The two subscription-answer sensor polls (ADR 0064).
Definition hub_core.h:1157
void set_tx_power(uint8_t power)
Set transmit power (dBm).
Definition hub_core.h:228
InternalGPIOPin * dio4_pin_
SX1276 DIO4 preamble detect (optional).
Definition hub_core.h:1573
virtual void set_key_extraction_armed(bool armed)
Arm or disarm the "Recover System Key" (key extraction) responder.
Definition hub_core.h:451
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:1513
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:323
void api_force_open_device_(const std::string &device_id)
Native API callback: force-open a registered cover device.
Definition hub_core.h:1499
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.
virtual void queue_set_fan_position(const std::string &device_id, uint8_t position)
Async form of set_fan_position() that keeps radio work serialized on the main loop.
bool poll_dhw_device_(const std::string &device_id)
Poll a water heater: the status read for its target, capabilities and boost and away state (read_dhw_...
void set_node_id(const std::string &id)
Set the controller's node ID (hex string).
Definition hub_core.h:224
void listen_for_key_extraction_peer_()
One idle pass of listening for the key-extraction responder's peer, in place of the generic idle hop ...
Definition hub_core.cpp:489
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:331
virtual bool set_device_tilt(const std::string &device_id, uint8_t tilt_percent)
Send a tilt command to a tilt‑capable cover.
virtual bool set_water_heater_mode(const std::string &device_id, DhwMode mode)
Set a water heater's operating mode: a write of register 0x0006 (encode_dhw_mode_write()).
InternalGPIOPin * dio1_pin_
SX1262 DIO1 interrupt; also carries the LR1121's DIO9 IRQ line.
Definition hub_core.h:1574
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:105
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:1670
virtual bool send_heating_command(const std::string &device_id, HeatingFunction fn, float value)
Run one heating function on the heater's private register protocol (CMD_WRITE_PRIVATE 0x20): a write ...
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:358
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...
virtual void queue_request_device_sun_level(const std::string &device_id)
Queue an async sun level poll; executed in loop() as a background operation.
void api_scan_paired_devices_()
Native API callback: broadcast a roll-call scan of already-paired devices.
Definition hub_core.h:1503
void set_rst_pin(InternalGPIOPin *pin)
Set the radio reset pin.
Definition hub_core.h:204
virtual bool apply_optimistic_stop(const std::string &device_id)
Predict that a device has stopped (e.g.
Definition hub_core.h:296
bool execute_dhw_(const std::string &device_id, const char *action, const std::function< bool(IoFrame &, const IoDevice &)> &build)
Send one water heater EXECUTE (target or operating mode) through try_execute_operation_(),...
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...
uint32_t device_event_count_
Device events recorded since boot, the last one's text and its subscriber; see record_device_event_()...
Definition hub_core.h:1612
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:1308
bool request_event_reading_(const std::string &device_id, EventReading kind)
Shared body of request_device_sun_level() and request_device_battery_level(): defer while the device ...
bool record_probe_reply_event(const IoFrame &reply)
Record a diagnostic probe's reply on the "Last Device Event" sensor when it is a device event report ...
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:569
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:260
bool set_dhw_operating_mode_(const std::string &device_id, DhwOperatingFeature which, bool on)
Switch a water heater's boost or away through the operating-mode word (ADR 0081).
virtual void register_device_callback(DeviceUpdateCallback cb)
Register a callback invoked when any device updates.
Definition hub_core.h:521
void set_pa_pin(uint8_t pa_pin)
Set PA boost pin configuration.
Definition hub_core.h:230
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 bool set_heating_temperature(const std::string &device_id, uint16_t main_parameter, const HeatingFp &fp)
Command a heater that carries a temperature or an alias in its main parameter (the heating temperatur...
virtual IoDevice * get_device(const std::string &device_id)
Retrieve a device by ID; returns nullptr if not found.
Definition hub_core.cpp:402
void follow_linked_remote_press_(const OneWayFrameInfo &info, const std::string &src_id)
React to a deduplicated 1W press from a remote: apply the optimistic state to the devices it controls...
void note_status_reported_(const std::string &device_id)
The device just reported its status: move its status refresh (if it has one) a whole interval out.
Definition hub_core.cpp:374
void queue_due_catchup_reads_()
Read every device with linked_remotes once, when a burst of linked-remote activity has been followed ...
Definition hub_core.cpp:528
bool diagnostic_probes_enabled() const
Whether diagnostic probes are enabled for this build.
Definition hub_core.h:494
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:1517
void update_tuning_number(const std::string &name, float value)
Receive a numeric tuning update from a HA number entity.
Definition hub_core.cpp:243
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:181
bool read_dhw_reading_(const std::string &device_id, uint8_t index, uint8_t max_tries)
Read one family-0x00 register of a water heater and fold its value into IoDevice::dhw.
PeriodicPollPolicy battery_level_poll_policy_
Schedule of the opt-in battery level poll.
Definition hub_core.h:1622
virtual void add_device(const std::string &device_id)
Add a device to the registry by device ID only (undeclared/legacy path).
Definition hub_core.cpp:396
void set_fem_en_pin(InternalGPIOPin *pin)
Set the front‑end module enable pin.
Definition hub_core.h:214
virtual void set_oneway_key_adoption_armed(bool armed)
Arm or disarm the 1W controller-key adoption listener.
Definition hub_core.h:469
InternalGPIOPin * vfem_pin_
Front-end module power.
Definition hub_core.h:1577
TuningConfig tuning_
Runtime tuning overrides.
Definition hub_core.h:1604
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:1479
virtual void set_device_status_poll_interval(const std::string &device_id, uint32_t poll_interval_ms)
Configure the optional follow-up polling interval for a registered device.
Definition hub_core.cpp:343
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,...
bool read_private_register_(const std::string &device_id, uint8_t reg_hi, uint8_t reg_lo, const char *label, IoFrame &response, uint8_t max_tries)
Read one private register raw: build the read request, run exchange_heating_() and check that the rep...
const IoDevice * water_heater_for_write_(const std::string &device_id, const char *operation)
The water heater device_id names, or nullptr when it is unknown, the hub is not set up yet,...
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:305
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:266
virtual bool read_heating_register(const std::string &device_id, HeatingFunction fn, float *value_out, uint8_t max_tries)
Read one register of a heater and fold the decoded value into IoDevice::heating.
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:278
void set_device_event_callback(std::function< void()> cb)
Register a callback invoked after every recorded device event, used by the "Last Device Event" text s...
Definition hub_core.h:437
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:1075
decisions::AttemptRequest cover_poll_attempts_(const std::string &device_id, bool scheduler_managed, bool state_unknown=false)
Attempts for a cover's status read: its read kind (a settling read or a look at a busy device,...
void set_dio1_pin(InternalGPIOPin *pin)
Set the DIO1 interrupt pin (SX1262; also carries the LR1121's DIO9 IRQ line).
Definition hub_core.h:210
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:620
const PairingTelemetry & pairing_telemetry() const
Definition hub_core.h:411
virtual void set_device_travel_speed(const std::string &device_id, TravelSpeed speed)
Select a device's travel speed at runtime (see IOHomeCoverTravelSpeedSelect and IOHomeCoverSilentSwit...
Definition hub_core.cpp:404
bool diagnostic_probes_enabled_
Whether diagnostic probes (DiagnosticProbes::probe_device()/probe_sweep()) are enabled.
Definition hub_core.h:1618
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:1523
virtual bool request_device_battery_level(const std::string &device_id)
Subscribe the device to EVENT_BATTERY_LEVEL_CHANGE and publish the level its answer carries.
virtual void queue_request_device_name(const std::string &device_id)
Queue an async device-name request; returns immediately, executed in loop().
ExchangeOutcome send_and_receive_(const IoFrame &request, IoFrame &response, uint32_t freq, const decisions::AttemptRequest &attempts=EXCHANGE_RETRY_COUNT)
Main request/response exchange with retry and automatic authentication.
Definition hub_core.cpp:313
virtual void queue_request_device_limitation(const std::string &device_id)
Queue an async minimum-limitation read for the rain sensor poll; executed in loop() as a background o...
void resolve_start_preamble_default_()
Resolve normal_start_preamble from the driver when YAML did not set it (ADR 0042).
Definition hub_core.cpp:229
void set_radio_test_mode(bool active)
Suspend the hub's normal loop (packet processing, hopping, polling).
Definition hub_core.h:197
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:1633
void set_radio_type(const std::string &type)
Set radio type ("sx1276", "sx1262", or "lr1121"); required by the YAML schema.
Definition hub_core.h:232
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:1509
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.
uint32_t last_pairing_end_ms_
millis() when the last pairing attempt ended; 0 before the first one (see queue_discover_and_pair()).
Definition hub_core.h:1602
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.
PeriodicPollPolicy & event_reading_policy_(EventReading kind)
The schedule of kind's poll.
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 poll_programme_period_(const std::string &device_id, const HeatingProfile &profile, uint8_t max_tries)
Read the temperature in force of a heater's programme, when the programme runs.
void loop() override
Main loop: process pending operations and drive radio state machine.
Definition hub_core.cpp:410
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:1476
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:417
ExchangeOutcome exchange_past_device_event_(const IoFrame &request, IoFrame &response, const decisions::AttemptRequest &attempts)
send_and_receive_() on the command channel, except that a device event report (decisions::is_device_e...
virtual bool set_water_heater_boost(const std::string &device_id, bool on)
Start or end a water heater's boost: a duration write of register 0x0000, then an EXECUTE with the op...
PairingTelemetry pairing_telemetry_
Per-attempt pairing telemetry.
Definition hub_core.h:1627
void record_device_event_(const IoFrame &frame)
Record a device event report, or a frame from a registered device that nothing else here handles: log...
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:394
OneWayTransmitter oneway_transmitter_
Owns the 1W controller identities, their rolling-sequence counters and the transmit burst.
Definition hub_core.h:1641
decisions::AttemptRequest heater_poll_attempts_(const std::string &device_id, bool scheduler_managed)
Attempts for a heater's status read: the count-based budget heaters keep (decisions::scheduled_poll_m...
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:666
void add_oneway_command_report_callback(OneWayCommandReportFn callback)
Subscribe to the report emitted after every 1W command attempt.
Definition hub_core.h:403
std::string radio_failure_reason_
Why setup() gave up on the radio, or empty.
Definition hub_core.h:1587
void settle_ended_status_tracking_()
Settle every device whose polling window ran out (StatusPollPolicy::pop_ended_device()).
virtual void set_device_rain_poll_interval(const std::string &device_id, uint32_t interval_ms)
Opt a registered device into the periodic rain sensor poll (a read of its minimum limitation).
Definition hub_core.cpp:349
std::string radio_type_
"sx1276", "sx1262", or "lr1121"; required by the YAML schema.
Definition hub_core.h:1584
virtual void set_device_status_refresh_interval(const std::string &device_id, uint32_t interval_ms)
Opt a registered device into the status refresh at rest: a status read whenever the device has not re...
Definition hub_core.cpp:368
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:1493
virtual bool request_device_limitation(const std::string &device_id)
Read the device's minimum limitation and derive its rain sensor state from the reply.
float get_setup_priority() const override
Get setup priority (HARDWARE to initialize early).
Definition hub_core.h:177
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 listen window.
Definition hub_core.h:986
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 act_on_foreign_frame_(const std::string &device_id, ForeignFrameKind kind, const std::string &controller, uint8_t cmd)
Decide, and act on, what a frame of another controller means for one registered device: a heard write...
void set_busy_pin(InternalGPIOPin *pin)
Set the BUSY pin (SX1262/LR1121).
Definition hub_core.h:212
void queue_due_periodic_polls_()
Queue every periodic poll that is due: the sensor polls (rain, sun level, battery level) and the stat...
Definition hub_core.cpp:543
virtual bool queue_device_command(const std::string &device_id, CoverCommand cmd)
Queue an async named command (STOP, FAVORITE, VENT, PARTIAL, PEDESTRIAN, FORCE_OPEN); returns immedia...
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:607
virtual void queue_request_device_battery_level(const std::string &device_id)
Queue an async battery level poll; executed in loop() as a background operation.
virtual bool set_water_heater_target(const std::string &device_id, float celsius)
Set a water heater's target temperature.
void apply_status_update_(const std::string &id, IoDevice &dev, const IoFrame &frame)
Handle an authenticated CMD_STATUS_UPDATE from a registered device: a device event report (decisions:...
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:164
void apply_event_reading_(const std::string &device_id, EventReading kind, const std::optional< uint16_t > &reading, bool unanswered)
Fold one poll outcome into the device record: a reading replaces the value and clears the miss count;...
bool poll_heating_device_(const std::string &device_id)
Poll a heater: read its profile's poll registers (stopping at the first failure), then the programme'...
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:1505
uint8_t spi_read() override
Read one byte (MISO only).
Definition hub_core.h:193
void api_identify_device_(const std::string &device_id)
Native API callback: trigger a registered device's physical identify.
Definition hub_core.h:1497
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:308
virtual bool set_heating_level(const std::string &device_id, uint8_t level)
Set the heating level of a heater that is commanded with levels (the plain electrical heater).
const std::string & last_device_event() const
Definition hub_core.h:425
void set_dio0_pin(InternalGPIOPin *pin)
Set the DIO0 interrupt pin (SX1276).
Definition hub_core.h:206
void set_tuning_config(const TuningConfig &config)
Apply the tuning configuration generated from YAML / UI entities.
Definition hub_core.h:239
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:369
bool radio_test_mode_
When true, loop() is suspended for loopback testing.
Definition hub_core.h:1603
void set_vfem_pin(InternalGPIOPin *pin)
Set the VFEM power pin.
Definition hub_core.h:216
ExchangeEngine exchange_engine_
Owns all authenticated exchange and LBT/hop logic.
Definition hub_core.h:1634
RequestResult exchange_and_record_(const std::string &device_id, const IoFrame &request, IoFrame &response, const RequestOptions &options)
Run one authenticated request/response exchange on the standard command channel and record its outcom...
RequestResult
What exchange_and_record_() found.
Definition hub_core.h:1195
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:310
const OneWayControllerRegistry & oneway_controllers() const
Definition hub_core.h:336
void schedule_heating_poll_(const std::string &device_id, uint32_t delay_ms)
Arm the heater's regular-cadence poll timer.
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 DiagnosticProbes::probe_sweep()).
Definition hub_core.h:651
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:476
bool oneway_key_adoption_armed() const
Whether the 1W key-adoption listener is currently armed.
Definition hub_core.h:482
PeriodicPollPolicy rain_poll_policy_
Schedule of the opt-in rain sensor poll.
Definition hub_core.h:1620
bool radio_failure_reported_
Set once loop() has reported a radio that latched a failure after setup, so the report (an error line...
Definition hub_core.h:1591
virtual bool request_device_sun_level(const std::string &device_id)
Subscribe the device to EVENT_LOCAL_SENSOR and publish the sun value its answer carries.
virtual bool set_light_state(const std::string &device_id, bool on)
Semantic binary helper for light entities.
bool write_dhw_register_(const std::string &device_id, const char *operation, uint8_t index, const uint8_t *payload, size_t payload_len)
Write one family-0x00 register of a water heater, then read it back.
void apply_rain_poll_result_(const std::string &device_id, RainSensorState reading, const std::string &limit_text, bool unanswered)
Fold one rain poll outcome into the device's rain sensor state and tell subscribers.
virtual void set_device_sun_poll_interval(const std::string &device_id, uint32_t interval_ms)
Opt a registered device into the periodic sun level poll (a subscription to EVENT_LOCAL_SENSOR,...
Definition hub_core.cpp:355
esphome::home_io_control::ManagementActionResult ManagementActionResult
Result payload used by hub-level management actions such as rename.
Definition hub_core.h:167
PairingEngine pairing_engine_
Owns the three-phase device pairing flow.
Definition hub_core.h:1635
bool read_dhw_register_(const std::string &device_id, uint8_t index, const char *label, IoFrame &response, uint8_t max_tries)
Read one family-0x00 register of a water heater raw (read_private_register_() gated on device_feature...
InternalGPIOPin * dio0_pin_
SX1276 DIO0 interrupt.
Definition hub_core.h:1572
void set_system_key(const std::string &key)
Set the system key (hex string).
Definition hub_core.h:226
ManagementActions management_actions_
Owns rename, identify, force-open, scan_paired_devices, and other hub-level HA actions.
Definition hub_core.h:1636
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 spi_write(uint8_t data) override
Write one byte (MOSI only).
Definition hub_core.h:190
std::vector< OneWayCommandReportFn > oneway_report_callbacks_
Subscribers to the per-command 1W report; one per "Last 1W Command" sensor.
Definition hub_core.h:1662
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:236
OnewayKeyAdoption oneway_key_adoption_
Opt-in, receive-only 1W controller-key adoption listener (oneway_key_adoption.cpp).
Definition hub_core.h:1646
void notify_device_update_(const std::string &id)
Fire all registered device update callbacks for the given device ID.
Definition hub_core.cpp:339
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:599
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:293
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:208
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:1666
bool exchange_heating_(const std::string &device_id, const char *label, const IoFrame &request, IoFrame &response, uint8_t max_tries)
The exchange every heating frame shares: send on the command channel, record the outcome (exchange co...
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:384
std::function< void()> pairing_result_callback_
Invoked once after every pairing attempt completes; see set_pairing_result_callback().
Definition hub_core.h:1610
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:77
KeyExtractionResponder key_extraction_
Device-role responder for the "Recover System Key" feature (key_extraction_responder....
Definition hub_core.h:1652
virtual bool set_water_heater_away(const std::string &device_id, bool on)
Start or end a water heater's away (holiday) mode: a duration write of register 0x0005 (255,...
bool read_dhw_status_(const std::string &device_id, uint8_t max_tries)
Read a water heater's target, capabilities and boost and away state: the status read DHW_STATUS_FPI1 ...
uint8_t spi_transfer(uint8_t data) override
Transfer one byte full‑duplex.
Definition hub_core.h:187
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:1608
void check_heater_model_(const std::string &device_id, uint8_t max_tries)
The one-off model check of a heater kind that has model rows (device_offers_models()).
bool act_on_overheard_request_(const IoFrame &frame)
Hand another controller's frame to act_on_foreign_frame_(): per the registered device it is addressed...
uint8_t tcxo_voltage_
SX1262/LR1121 TCXO voltage setting (default 1.8 V).
Definition hub_core.h:1596
virtual void set_device_temperature_range(const std::string &device_id, float min_c, float max_c)
Set the temperature encoding range of a heater that carries a temperature in its main parameter (YAML...
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, const decisions::AttemptRequest &attempts=EXCHANGE_RETRY_COUNT)
Shared request/response helper for high-level operations: exchange_and_record_() plus update_device_s...
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:285
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, PARTIAL, PEDESTRIAN, FORCE_OPEN) via the authen...
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:222
bool authenticate_request_(const IoFrame &request, uint32_t freq)
Handle an inbound authenticated command from a device (status updates, etc.).
Definition hub_core.cpp:335
void spi_disable() override
Disable the SPI bus.
Definition hub_core.h:183
InternalGPIOPin * busy_pin_
SX1262/LR1121 BUSY pin.
Definition hub_core.h:1575
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:1491
void apply_tuning_to_radio_()
Apply the current tuning configuration to the active radio driver.
Definition hub_core.cpp:217
FemProfile fem_profile_
Which FEM part fem_pa_pin_ belongs to, if any.
Definition hub_core.h:1579
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:1675
PeriodicPollPolicy status_refresh_policy_
Schedule of the opt-in status refresh at rest.
Definition hub_core.h:1623
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:274
bool radio_stopped_responding() const
True once loop() has seen the radio latch a failure after setup (a runtime BUSY timeout,...
Definition hub_core.h:513
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 DiagnosticP...
Definition hub_core.h:640
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:458
PeriodicPollPolicy sun_poll_policy_
Schedule of the opt-in sun level poll.
Definition hub_core.h:1621
void note_foreign_challenge_(const IoFrame &frame)
Hand a registered device's challenge (0x3C) to another controller to act_on_foreign_frame_().
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.
bool record_if_device_event_(const IoFrame &frame, bool reached_unhandled_fallback)
Record frame as a device event when it qualifies: its source is a registered device and it is either ...
void set_diagnostic_probes_enabled(bool enabled)
Set whether DiagnosticProbes::probe_device()/probe_sweep() are allowed to run.
Definition hub_core.h:491
virtual void set_device_battery_level_poll_interval(const std::string &device_id, uint32_t interval_ms)
Opt a registered device into the periodic battery level poll (a subscription to EVENT_BATTERY_LEVEL_C...
Definition hub_core.cpp:361
void set_fem_pa_pin(InternalGPIOPin *pin)
Set the FEM PA switch pin.
Definition hub_core.h:218
RadioDriver * get_radio() const
Get the underlying radio driver (for diagnostics and test tooling).
Definition hub_core.h:200
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 a paired device; delegates to DiagnosticProbes::probe_device(...
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 per index; delegates to DiagnosticProbes::probe_sweep().
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 and the sensor polls) that yield to cont...
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.
Per-device periodic poll schedule, miss retry and miss counter for one poll kind.
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.
RainSensorState
What the hub's latest minimum-limitation read says about a rain-protection limit.
DhwMode
Operating mode as register 0x0006 holds it.
Definition proto_dhw.h:145
HeatingFunction
Registers and functions of a 2W heater, one row of the descriptor table each.
ForeignFrameKind
What an overheard frame of another controller is.
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:67
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.
DhwOperatingFeature
The two features the operating-mode word switches; dhw_operating_feature_wire() holds their wire fact...
Definition proto_dhw.h:115
DeviceType
IO-Homecontrol node types.
std::string format_position(float pos)
Format a position float as a human‑readable string (e.g.
Definition hub_core.h:1685
static constexpr uint32_t PAIRING_STALE_PRESS_MS
How long after a pairing attempt ends a Discover & Pair press counts as made during it.
Definition hub_core.h:84
ExchangeOutcome
Authenticated exchange engine — outbound and inbound protocol flows.
@ FAILED
No usable reply; the device may never have heard the request.
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:65
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.
TravelSpeed
The speed a cover's position moves ask the motor for (ADR 0065).
void clear_moving_evidence(IoDevice &dev)
Forget that a device was travelling: it reported itself stopped (including in the ack to a STOP),...
constexpr uint8_t DEFAULT_PA_PIN_PA_BOOST
SX1276 PA_CONFIG selector for the PA_BOOST output path.
Definition hub_core.h:64
constexpr uint8_t DEFAULT_TX_POWER_DBM
Default TX power used unless YAML overrides it.
Definition hub_core.h:63
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.
Schedule and failure bookkeeping for the opt-in periodic polls.
Device-name, address-classification and 1W-frame codecs.
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
Pure codec for the private register protocol of 2W heaters (CMD_WRITE_PRIVATE 0x20).
Heating profiles: what a climate card offers for each kind of heater, and how.
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.
A functional parameter and its value.
Everything one execute-family operation needs beyond its own guard and frame builder.
Definition hub_core.h:1249
const char * action
Verb/phrase for the "Sending ..." and rejection logs.
Definition hub_core.h:1250
bool settle_as_stop
Passed through to arm_execute_confirmation_poll_().
Definition hub_core.h:1251
Per-request knobs of exchange_and_record_().
Definition hub_core.h:1203
uint32_t retry_after_fail_ms
If non-zero, drives the status-poll backoff and failure streaks.
Definition hub_core.h:1205
decisions::AttemptRequest attempts
Attempt cap or read kind forwarded to send_and_receive_().
Definition hub_core.h:1206
bool warn_on_no_response
Log a warning when no response arrives.
Definition hub_core.h:1204
Runtime state of a paired IO‑Homecontrol device.
bool has_system_key
YAML-declared (system_key:): this device keeps a key of its own.
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.
What a caller asks of one exchange's attempts: a plain cap on tries (today's shape),...
Key fields of the last processed 1W frame, used to collapse a remote's repeat burst.
Runtime tuning configuration for pairing and radio diagnostics.