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() processes the pending_operations_ queue (serializes all radio work).
19/// - All outbound commands go through send_and_receive_ which handles retry & auth.
20/// - Inbound frames are processed in process_received_packet_ and may trigger
21/// inbound authentication (hub_exchange.h) if the device proves itself.
22/// - Device registry and callbacks provide fan‑out to platform entities (covers/lights/switches).
23
24#include "esphome/core/component.h"
25#include "esphome/core/hal.h"
26#include "esphome/components/api/custom_api_device.h"
27#include "esphome/components/spi/spi.h"
28#include "esphome/components/button/button.h"
29#include "proto_frame.h"
30#include "radio_interface.h"
31#include "hub_exchange.h"
32#include "hub_decisions.h"
33#include "hub_pairing.h"
34#include <deque>
35#include <map>
36#include <vector>
37#include <functional>
38
39namespace esphome {
40namespace home_io_control {
41
42namespace detail {
43class RenameDeviceServiceDescriptor;
44}
45
46/// Callback type for notifying covers of device state changes.
47using DeviceUpdateCallback = std::function<void(const std::string &device_id, const IoDevice &device)>;
48
49inline constexpr uint8_t DEFAULT_TX_POWER_DBM = 17; ///< Default TX power used unless YAML overrides it.
50inline constexpr uint8_t DEFAULT_PA_PIN_PA_BOOST = 0x80; ///< SX1276 PA_CONFIG selector for the PA_BOOST output path.
51inline constexpr uint8_t DEFAULT_TCXO_VOLTAGE_SETTING_1P8V = 0x03; ///< SX1262 DIO3 setting value for a 1.8 V TCXO.
52inline constexpr size_t POSITION_TEXT_BUFFER_SIZE = 16; ///< Buffer for formatted position strings such as "100%".
53
54// ============================================================================
55// Main Component
56// ============================================================================
57
58/// The main IO-Homecontrol component. Manages the protocol layer and delegates
59/// radio operations to a RadioDriver instance.
60///
61/// Inherits SPIDevice so that ESPHome's Python codegen can configure SPI pins.
62/// Implements SpiAccess to provide the radio driver with SPI bus access.
63/// @ingroup hioc_hub
64class IOHomeControlComponent : public Component,
65 public api::CustomAPIDevice,
66 public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
67 spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_8MHZ>,
68 public SpiAccess {
70
71 public:
72 /// @brief Result payload used by hub-level management actions such as rename.
74 bool success{false}; ///< Whether the requested management action succeeded.
75 bool verified{false}; ///< Whether a follow-up readback verified the applied state.
76 bool has_result_code{false}; ///< True when result_code contains a decoded CMD_ERROR_RESP byte.
77 uint8_t result_code{0}; ///< Optional CMD_ERROR_RESP result byte from the device.
78 std::string action; ///< Action name, for example "rename_device".
79 std::string device_id; ///< Target IO-homecontrol device ID.
80 std::string message; ///< Human-readable outcome summary.
81 std::string requested_name; ///< Requested normalized UTF-8 name for rename actions.
82 std::string applied_name; ///< Verified cached UTF-8 name after a readback, when available.
83 };
84
85 /// @brief Initialize hardware (radio and device registry).
86 void setup() override;
87 /// @brief Main loop: process pending operations and drive radio state machine.
88 void loop() override;
89 /// @brief Dump configuration and radio debug info to the log.
90 void dump_config() override;
91 /// @brief Get setup priority (HARDWARE to initialize early).
92 /// @return setup_priority::HARDWARE.
93 [[nodiscard]] float get_setup_priority() const override { return setup_priority::HARDWARE; }
94
95 // --- SpiAccess implementation (delegates to SPIDevice) ---
96 /// @brief Enable the SPI bus.
97 void spi_enable() override { this->enable(); }
98 /// @brief Disable the SPI bus.
99 void spi_disable() override { this->disable(); }
100 /// @brief Transfer one byte full‑duplex.
101 /// @param data Byte to send.
102 /// @return Received byte.
103 uint8_t spi_transfer(uint8_t data) override { return this->transfer_byte(data); }
104 /// @brief Write one byte (MOSI only).
105 /// @param data Byte to send.
106 void spi_write(uint8_t data) override { this->write_byte(data); }
107 /// @brief Read one byte (MISO only).
108 /// @return Received byte.
109 uint8_t spi_read() override { return this->read_byte(); }
110
111 // --- YAML configuration setters (called by generated code) ---
112 /// Set the radio reset pin.
113 void set_rst_pin(InternalGPIOPin *pin) { this->rst_pin_ = pin; }
114 /// Set the DIO0 interrupt pin (SX1276).
115 void set_dio0_pin(InternalGPIOPin *pin) { this->dio0_pin_ = pin; }
116 /// Set the DIO4 preamble‑detect pin (SX1276, optional).
117 void set_dio4_pin(InternalGPIOPin *pin) { this->dio4_pin_ = pin; }
118 /// Set the DIO1 interrupt pin (SX1262).
119 void set_dio1_pin(InternalGPIOPin *pin) { this->dio1_pin_ = pin; }
120 /// Set the BUSY pin (SX1262).
121 void set_busy_pin(InternalGPIOPin *pin) { this->busy_pin_ = pin; }
122 /// Set the front‑end module enable pin.
123 void set_fem_en_pin(InternalGPIOPin *pin) { this->fem_en_pin_ = pin; }
124 /// Set the VFEM power pin.
125 void set_vfem_pin(InternalGPIOPin *pin) { this->vfem_pin_ = pin; }
126 /// Set the FEM PA switch pin.
127 void set_fem_pa_pin(InternalGPIOPin *pin) { this->fem_pa_pin_ = pin; }
128 /// Set the controller's node ID (hex string).
129 void set_node_id(const std::string &id) { this->node_id_str_ = id; }
130 /// Set the system key (hex string).
131 void set_system_key(const std::string &key) { this->system_key_str_ = key; }
132 /// Set transmit power (dBm).
133 void set_tx_power(uint8_t power) { this->tx_power_ = power; }
134 /// Set PA boost pin configuration.
135 void set_pa_pin(uint8_t pa_pin) { this->pa_pin_ = pa_pin; }
136 /// Set radio type ("sx1276" or "sx1262"); empty string means auto‑detect.
137 void set_radio_type(const std::string &type) { this->radio_type_ = type; }
138 /// Set TCXO voltage for SX1262 (1.8V / 3.3V).
139 void set_tcxo_voltage(uint8_t voltage) { this->tcxo_voltage_ = voltage; }
140
141 /// Declare that a remote (identified by its node ID) controls a registered device.
142 /// When activity from this remote is overheard, a status poll is scheduled for the device.
143 /// This is needed for 1W remotes whose destination address differs from the device's 2W ID.
144 /// @param remote_id Node ID of the remote control.
145 /// @param device_id Node ID of the device it controls.
146 void add_linked_remote(const std::string &remote_id, const std::string &device_id) {
147 this->linked_remotes_[remote_id].push_back(device_id);
148 }
149
150 // --- Device management (called by platform entities during setup) ---
151 /// Add a device to the registry by device ID only (legacy/delegating overload).
152 /// Type, subtype, and inverted default to UNKNOWN / 0 / false; use the 4-arg
153 /// overload when type/subtype/inverted come from YAML declarations.
154 /// @param device_id Hexadecimal node ID string.
155 virtual void add_device(const std::string &device_id);
156 /// Add a device to the registry with full metadata from YAML.
157 /// @param device_id Hexadecimal node ID string.
158 /// @param type Device type from YAML declaration (UNKNOWN if not specified).
159 /// @param subtype Device subtype from YAML declaration.
160 /// @param inverted Position inversion flag from YAML declaration.
161 virtual void add_device(const std::string &device_id, DeviceType type, uint8_t subtype, bool inverted);
162 /// Retrieve a device by ID; returns nullptr if not found.
163 /// @param device_id Hexadecimal node ID.
164 /// @return Pointer to IoDevice, or nullptr.
165 virtual IoDevice *get_device(const std::string &device_id);
166 /// Register a callback invoked when any device updates.
167 /// @param cb Callable with signature void(const std::string&, const IoDevice&).
168 virtual void register_device_callback(DeviceUpdateCallback cb) { this->callbacks_.push_back(std::move(cb)); }
169 /// Configure the optional follow-up polling interval for a registered device.
170 /// @param device_id Target device ID.
171 /// @param poll_interval_ms Poll interval in milliseconds; zero keeps the legacy one-shot settle poll only.
172 virtual void set_device_status_poll_interval(const std::string &device_id, uint32_t poll_interval_ms);
173
174 // --- High-level operations ---
175 /// Send a position command to a device.
176 /// @param device_id Target device ID.
177 /// @param position Desired position (0–100, or POS_STOP/POS_FAVORITE).
178 /// @return true if device acknowledged; false on timeout or radio error.
179 virtual bool set_device_position(const std::string &device_id, uint8_t position);
180 /// Send a tilt command to a tilt‑capable cover.
181 /// @param device_id Target device ID.
182 /// @param tilt_percent Desired tilt (0–100).
183 /// @return true if device acknowledged; false otherwise.
184 virtual bool set_device_tilt(const std::string &device_id, uint8_t tilt_percent);
185 /// Request current status from a device.
186 /// @param device_id Target device ID.
187 /// @return true if status frame was received and processed.
188 virtual bool request_device_status(const std::string &device_id);
189 /// Request the stored device name from a device.
190 /// @param device_id Target device ID.
191 /// @return true if a name response frame was received and processed.
192 virtual bool request_device_name(const std::string &device_id);
193 /// Rename a device and verify the result by reading the name back.
194 /// @param device_id Target device ID.
195 /// @param new_name Requested UTF-8 device name.
196 /// @return Structured result describing success, verification, and any validation failure.
197 virtual ManagementActionResult rename_device(const std::string &device_id, const std::string &new_name);
198 /// Discover and pair a device that is in pairing mode.
199 /// @return true if pairing completed successfully; false otherwise.
200 virtual bool discover_and_pair();
201 /// Semantic binary helper for light entities. Internally mapped to the shared execute path.
202 /// @param device_id Target device ID.
203 /// @param on Desired on/off state.
204 /// @return true if device acknowledged.
205 virtual bool set_light_state(const std::string &device_id, bool on);
206 /// Semantic binary helper for switch entities. Internally mapped to the shared execute path.
207 /// @param device_id Target device ID.
208 /// @param on Desired on/off state.
209 /// @return true if device acknowledged.
210 virtual bool set_switch_state(const std::string &device_id, bool on);
211 /// Semantic lock helper for lock entities. Internally mapped to the shared execute path.
212 /// @param device_id Target device ID.
213 /// @param locked Desired locked/unlocked state.
214 /// @return true if device acknowledged.
215 virtual bool set_lock_state(const std::string &device_id, bool locked);
216 /// Queue an async position update; returns immediately, executed in loop().
217 /// @param device_id Target device ID.
218 /// @param position Desired position.
219 virtual void queue_set_device_position(const std::string &device_id, uint8_t position);
220 /// Queue an async tilt update; returns immediately, executed in loop().
221 /// @param device_id Target device ID.
222 /// @param tilt_percent Desired tilt.
223 virtual void queue_set_device_tilt(const std::string &device_id, uint8_t tilt_percent);
224 /// Queue an async status request; returns immediately, executed in loop().
225 /// @param device_id Target device ID.
226 virtual void queue_request_device_status(const std::string &device_id);
227 /// Queue an async device-name request; returns immediately, executed in loop().
228 /// @param device_id Target device ID.
229 virtual void queue_request_device_name(const std::string &device_id);
230 /// Queue a pairing operation; executed in loop() when radio idle.
231 virtual void queue_discover_and_pair();
232 /// Async form of set_light_state() that keeps radio work serialized on the main loop.
233 /// @param device_id Target device ID.
234 /// @param on Desired on/off state.
235 virtual void queue_set_light_state(const std::string &device_id, bool on);
236 /// Async form of set_switch_state() that keeps radio work serialized on the main loop.
237 /// @param device_id Target device ID.
238 /// @param on Desired on/off state.
239 virtual void queue_set_switch_state(const std::string &device_id, bool on);
240 /// Async form of set_lock_state() that keeps radio work serialized on the main loop.
241 /// @param device_id Target device ID.
242 /// @param locked Desired locked/unlocked state.
243 virtual void queue_set_lock_state(const std::string &device_id, bool locked);
244
245 protected:
246 // --- Protocol-level operations ---
247 /// Transmit a raw IoFrame on the current frequency with given preamble length.
248 /// @param frame IoFrame to transmit.
249 /// @param freq RF frequency in Hz.
250 /// @param preamble Preamble length in bytes (LONG_PREAMBLE or SHORT_PREAMBLE).
251 bool transmit_frame_(const IoFrame &frame, uint32_t freq, uint16_t preamble);
252 /// Main request/response exchange with retry and automatic authentication.
253 /// @param request Outbound request IoFrame.
254 /// @param response Output: received response IoFrame.
255 /// @param freq RF frequency in Hz.
256 /// @return true if exchange succeeded; false otherwise.
257 bool send_and_receive_(const IoFrame &request, IoFrame &response, uint32_t freq);
258 /// Handle an inbound authenticated command from a device (status updates, etc.).
259 /// @param request Inbound authenticated request (e.g., CMD_STATUS_UPDATE).
260 /// @param freq RF frequency the packet arrived on.
261 /// @return true if authentication succeeded; false otherwise.
262 bool authenticate_request_(const IoFrame &request, uint32_t freq);
263 /// Parse a received frame, merge supported device state or metadata, and notify callbacks.
264 /// @param packet Raw radio packet containing a parsed IoFrame.
265 void process_received_packet_(const RadioRxPacket &packet);
266 /// Extract supported position or metadata info from a response frame and merge it into the device record.
267 /// @param frame IoFrame containing a supported inbound command such as CMD_PRIVATE_RESP,
268 /// CMD_STATUS_UPDATE, CMD_GET_NAME_RESP, or CMD_GET_INFO2_RESP.
269 void update_device_status_(const IoFrame &frame);
270 /// Schedule a delayed status poll for a registered device using the Component timeout API.
271 /// @param device_id ID of the device to poll.
272 /// @param delay_ms Delay in milliseconds before polling.
273 /// @note Uses ESPHome's set_timeout() mechanism; the callback executes in loop().
274 /// A zero delay schedules immediately on the next loop iteration.
275 void schedule_status_poll_(const std::string &device_id, uint32_t delay_ms);
276 /// Begin bounded follow-up polling for a device after a command or overheard remote activity.
277 /// @param device_id ID of the device to poll.
278 /// @param initial_delay_ms Delay before the first follow-up poll.
279 void begin_status_poll_tracking_(const std::string &device_id, uint32_t initial_delay_ms);
280 /// Shared request/response helper for high-level operations.
281 /// @param device_id Target device ID.
282 /// @param request Outbound request frame.
283 /// @param warn_on_no_response If true, logs a warning when no response is received.
284 /// @param retry_after_fail_ms If non-zero, schedules next status poll after this delay on failure.
285 /// @return true if device acknowledged; false otherwise.
286 bool execute_request_and_update_(const std::string &device_id, const IoFrame &request, bool warn_on_no_response,
287 uint32_t retry_after_fail_ms = 0);
288 /// Register hub-level Home Assistant actions exposed through ESPHome's native API.
290 /// Publish the outcome of a management action as a Home Assistant event and structured logs.
291 /// @param result Management action result to emit.
292 void publish_management_result_(const ManagementActionResult &result);
293 /// Native API action callback: rename a registered device.
294 /// @param device_id Target device ID as provided by Home Assistant.
295 /// @param new_name Requested UTF-8 device name.
296 /// @note This callback is wired from the native API service descriptor and forwards
297 /// the decoded string arguments directly into rename_device().
298 void api_rename_device_(const std::string &device_id, const std::string &new_name);
299 /// Fire all registered device update callbacks for the given device ID.
300 /// @param id Device ID that updated.
301 void notify_device_update_(const std::string &id);
302 /// Pop next pending operation from the queue and execute it (set position, request status, discover).
304
305 // --- Outbound exchange helpers ---
306 /// Wrap transmit_frame_ and mark context failed on error.
307 /// @param request Outbound IoFrame to transmit.
308 /// @param freq RF frequency in Hz.
309 /// @param preamble Preamble length in bytes.
310 /// @param ctx Exchange context (state updated on failure).
311 /// @return true if transmit succeeded; false otherwise.
312 bool transmit_request_(const IoFrame &request, uint32_t freq, uint16_t preamble,
314 /// Wait loop for the first response packet; classifies via decisions::classify_exchange_first_response.
315 /// @param request Original request frame (used for endpoint matching).
316 /// @param ctx Exchange context (provides deadline and receives rx frame on accept).
317 /// @return Disposition indicating next step.
320 /// Perform challenge-response (TX auth response) after a 0x3C is received.
321 /// @param request Original request frame (needed for HMAC derivation).
322 /// @param freq RF channel frequency (same channel used for the request).
323 /// @param ctx Exchange context holding the challenge frame and state.
324 /// @return true if challenge response was sent successfully; false otherwise.
325 bool handle_authentication_(const IoFrame &request, uint32_t freq, exchange::OutboundExchangeContext &ctx);
326 /// Wait loop for the final authenticated response; uses is_valid_final_response().
327 /// @param request Original request frame (used for endpoint matching).
328 /// @param ctx Exchange context (receives final rx frame on accept).
329 /// @return ACCEPT if a matching final response arrives; IGNORE_UNRELATED on timeout.
332
333 // --- Pairing helpers ---
334 /// Wait for a discovery response (0x29) during pairing.
335 /// @param timeout_ms Maximum time to wait in milliseconds.
336 /// @param packet Output: raw RadioRxPacket of the accepted frame.
337 /// @param response_frame Output: parsed IoFrame of the accepted frame.
338 /// @return PairingDiscoveryDisposition: ACCEPT on success; NO_RESPONSE or INVALID otherwise.
340 IoFrame &response_frame);
341 /// Wait for a key-challenge (0x3C) from target device during pairing key exchange.
342 /// @param timeout_ms Maximum time to wait in milliseconds.
343 /// @param packet Output: raw RadioRxPacket of the challenge frame.
344 /// @param challenge_frame Output: parsed IoFrame containing the challenge.
345 /// @param device_node_id Node ID of the device we are pairing (expected sender).
346 /// @return true if a valid challenge was received; false on timeout.
347 bool wait_for_key_challenge_(uint32_t timeout_ms, RadioRxPacket &packet, IoFrame &challenge_frame,
348 const uint8_t device_node_id[NODE_ID_SIZE]);
349
350 /// Parse a discovery response frame into device metadata and ID.
351 /// @param frame Parsed discovery response.
352 /// @param device Output: populated IoDevice (node_id, type, subtype, inverted, position/target/stopped).
353 /// @param device_id Output: hex string representation of node ID.
354 static void parse_device_from_discovery(const IoFrame &frame, IoDevice &device, std::string &device_id);
355
356 // --- Pairing phase helpers ---
357 /// Phase 1: broadcast discovery (0x28) and wait for a device response (0x29).
358 /// @param context Pairing context modified on success.
359 /// @return PairingDiscoveryDisposition: ACCEPT, NO_RESPONSE, or INVALID.
361 /// Phase 2: authenticated key exchange (0x31 → 0x3C → 0x32 → 0x33).
362 /// @param context Pairing context populated by run_discovery_phase_().
363 /// @return true if key exchange completes successfully; false otherwise.
365 /// Phase 3: send SetConfig1 (0x6F) to finalize device configuration.
366 /// @param context Pairing context with device information.
367 /// @return true (pairing proceeds regardless of set‑config outcome).
369
370 /// @brief Type of queued pending operation for the main loop.
371 enum class PendingOperationType : uint8_t {
372 SET_POSITION, ///< Queue a set_device_position call (position 0–100 or special values).
373 SET_TILT, ///< Queue a set_device_tilt call (tilt percentage 0–100).
374 SET_LIGHT_STATE, ///< Queue a set_light_state call (binary on/off).
375 SET_LOCK_STATE, ///< Queue a set_lock_state call (locked/unlocked).
376 SET_SWITCH_STATE, ///< Queue a set_switch_state call (binary on/off).
377 REQUEST_STATUS, ///< Queue a request_device_status call (poll for current position).
378 REQUEST_NAME, ///< Queue a request_device_name call (poll for stored device name).
379 DISCOVER_AND_PAIR, ///< Queue a discover_and_pair call (starts 3‑phase pairing flow).
380 };
381
382 /// @brief A single queued operation to be processed in loop().
384 PendingOperationType type; ///< Operation type (determines which queue handler to invoke).
385 std::string device_id; ///< Target device ID (hex string, e.g., "123ABC").
386 uint8_t position{0}; ///< Position/tilt value (0–100) or binary state (ON/UNLOCK=0, OFF/LOCK=100).
387 };
388
389 /// @brief Debug snapshot of the last exchange attempt.
391 const char *stage{"idle"}; ///< Current stage name (e.g., "TX_REQUEST", "WAIT_FIRST_RESPONSE", "FAILED").
392 uint8_t tries{0}; ///< Try number (1‑based; increments on each retry within EXCHANGE_RETRY_COUNT).
393 uint8_t request_cmd{0}; ///< Command ID of the original request (e.g., CMD_EXECUTE=0x00).
394 bool saw_challenge{false}; ///< True if a challenge (0x3C) was seen during the exchange.
395 bool capture_valid{false}; ///< True if radio capture data is valid for the last packet seen.
396 bool capture_rx_done{false}; ///< True if RxDone interrupt fired (packet fully received).
397 bool capture_crc_error{false}; ///< True if CRC error flagged (SX1262 only; SX1276 IoHomeOn filters in hardware).
398 uint32_t capture_freq_hz{0}; ///< RF frequency of the captured packet (Hz).
399 uint16_t capture_irq_status{0}; ///< Raw IRQ status register value from the radio chip.
400 uint8_t capture_packet_status{0}; ///< Packet status byte (chip-specific; SX1262 includes CRC flag).
401 uint8_t capture_reported_len{0}; ///< Length reported by the radio's packet engine.
402 uint8_t capture_frame_len{0}; ///< Length of the parsed protocol frame after recovery/UART decoding.
403 int16_t capture_rssi_dbm{0}; ///< Received signal strength of the captured packet (dBm, negative).
404 };
405
406 void reset_exchange_debug_(uint8_t request_cmd);
407 void record_exchange_debug_(const char *stage, uint8_t tries, bool saw_challenge);
408 void log_exchange_debug_(const char *device_id) const;
409
410 // --- Frequency hopping ---
411 void hop_frequency_();
412
413 // --- Radio driver ---
415
416 // --- Hardware pins (set by YAML codegen, passed to radio driver in setup) ---
417 InternalGPIOPin *rst_pin_{nullptr};
418 InternalGPIOPin *dio0_pin_{nullptr}; ///< SX1276 DIO0 interrupt
419 InternalGPIOPin *dio4_pin_{nullptr}; ///< SX1276 DIO4 preamble detect (optional)
420 InternalGPIOPin *dio1_pin_{nullptr}; ///< SX1262 DIO1 interrupt
421 InternalGPIOPin *busy_pin_{nullptr}; ///< SX1262 BUSY pin
422 InternalGPIOPin *fem_en_pin_{nullptr}; ///< Front-end module enable
423 InternalGPIOPin *vfem_pin_{nullptr}; ///< Front-end module power
424 InternalGPIOPin *fem_pa_pin_{nullptr}; ///< Front-end module PA switch
425
426 // --- Configuration (from YAML) ---
427 std::string node_id_str_;
428 std::string system_key_str_;
429 std::string radio_type_; ///< "sx1276", "sx1262", or "" (auto-detect)
434 uint8_t tcxo_voltage_{DEFAULT_TCXO_VOLTAGE_SETTING_1P8V}; ///< SX1262 TCXO voltage setting (default 1.8 V)
435
436 // --- Runtime state ---
437 bool initialized_{false};
438 bool busy_{false};
439 uint32_t last_hop_us_{0};
441 std::map<std::string, IoDevice> devices_;
442 std::vector<DeviceUpdateCallback> callbacks_;
443 std::deque<PendingOperation> pending_operations_;
444 /// Maps remote node IDs to lists of device IDs they control.
445 /// Used to trigger status polls when 1W remote activity is overheard.
446 std::map<std::string, std::vector<std::string>> linked_remotes_;
447};
448
449// ============================================================================
450// Discover & Pair Button
451// ============================================================================
452
453/// Button entity that triggers device discovery and pairing when pressed in Home Assistant.
454/// @ingroup hioc_platforms
455class IOHomeDiscoverButton : public button::Button, public Component {
456 public:
457 void set_parent(IOHomeControlComponent *parent) { this->parent_ = parent; }
458 void dump_config() override {}
459
460 protected:
461 /// @brief When button is pressed, queue a discovery/pair operation.
462 void press_action() override { this->parent_->queue_discover_and_pair(); }
464};
465
466// ----------------------------------------------------------------------------
467// Test-visible helpers (inline for host unit tests)
468// ----------------------------------------------------------------------------
469
470/// Check if a stored node ID is valid (not all-zero, not all-0xFF).
471/// @param id 3‑byte node ID buffer.
472/// @return true if the ID is non-zero and non-0xFF.
473inline bool stored_node_id_is_valid(const uint8_t id[NODE_ID_SIZE]) {
474 bool all_zero = true;
475 bool all_ff = true;
476 for (uint8_t i = 0; i < NODE_ID_SIZE; i++) {
477 all_zero = all_zero && id[i] == 0;
478 all_ff = all_ff && id[i] == UINT8_MAX;
479 }
480 return !all_zero && !all_ff;
481}
482
483/// Format a position float as a human‑readable string (e.g. "50%", "unknown").
484/// @param pos Position value (0–100 or UNKNOWN_POSITION).
485/// @return String like "50%" or "unknown".
486inline std::string format_position(float pos) {
487 if (pos == UNKNOWN_POSITION) {
488 return "unknown";
489 }
491 snprintf(buf, sizeof(buf), "%.0f%%", pos);
492 return buf;
493}
494
495} // namespace home_io_control
496} // namespace esphome
The main IO-Homecontrol component.
Definition hub_core.h:68
InternalGPIOPin * fem_en_pin_
Front-end module enable.
Definition hub_core.h:422
InternalGPIOPin * fem_pa_pin_
Front-end module PA switch.
Definition hub_core.h:424
std::map< std::string, IoDevice > devices_
Definition hub_core.h:441
virtual bool set_lock_state(const std::string &device_id, bool locked)
Semantic lock helper for lock entities.
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.
void set_tx_power(uint8_t power)
Set transmit power (dBm).
Definition hub_core.h:133
InternalGPIOPin * dio4_pin_
SX1276 DIO4 preamble detect (optional).
Definition hub_core.h:419
void reset_exchange_debug_(uint8_t request_cmd)
Definition hub_core.cpp:37
void set_node_id(const std::string &id)
Set the controller's node ID (hex string).
Definition hub_core.h:129
virtual bool set_device_tilt(const std::string &device_id, uint8_t tilt_percent)
Send a tilt command to a tilt‑capable cover.
InternalGPIOPin * dio1_pin_
SX1262 DIO1 interrupt.
Definition hub_core.h:420
bool send_and_receive_(const IoFrame &request, IoFrame &response, uint32_t freq)
Main request/response exchange with retry and automatic authentication.
virtual bool set_switch_state(const std::string &device_id, bool on)
Semantic binary helper for switch entities.
bool transmit_request_(const IoFrame &request, uint32_t freq, uint16_t preamble, exchange::OutboundExchangeContext &ctx)
Wrap transmit_frame_ and mark context failed on error.
void set_rst_pin(InternalGPIOPin *pin)
Set the radio reset pin.
Definition hub_core.h:113
decisions::PairingDiscoveryDisposition run_discovery_phase_(pairing::PairingContext &context)
Phase 1: broadcast discovery (0x28) and wait for a device response (0x29).
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 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:307
virtual void register_device_callback(DeviceUpdateCallback cb)
Register a callback invoked when any device updates.
Definition hub_core.h:168
void set_pa_pin(uint8_t pa_pin)
Set PA boost pin configuration.
Definition hub_core.h:135
void record_exchange_debug_(const char *stage, uint8_t tries, bool saw_challenge)
Definition hub_core.cpp:42
virtual IoDevice * get_device(const std::string &device_id)
Retrieve a device by ID; returns nullptr if not found.
Definition hub_core.cpp:258
virtual void queue_request_device_status(const std::string &device_id)
Queue an async status request; returns immediately, executed in loop().
void spi_enable() override
Enable the SPI bus.
Definition hub_core.h:97
virtual void add_device(const std::string &device_id)
Add a device to the registry by device ID only (legacy/delegating overload).
Definition hub_core.cpp:239
void set_fem_en_pin(InternalGPIOPin *pin)
Set the front‑end module enable pin.
Definition hub_core.h:123
InternalGPIOPin * vfem_pin_
Front-end module power.
Definition hub_core.h:423
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:232
void process_pending_operation_()
Pop next pending operation from the queue and execute it (set position, request status,...
void schedule_status_poll_(const std::string &device_id, uint32_t delay_ms)
Schedule a delayed status poll for a registered device using the Component timeout API.
void hop_frequency_()
Hop to the next channel in the 3‑channel sequence: CH1 → CH2 → CH3 → CH1.
Definition hub_core.cpp:171
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:146
bool finalize_pairing_configuration_(pairing::PairingContext &context)
Phase 3: send SetConfig1 (0x6F) to finalize device configuration.
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)
Shared request/response helper for high-level operations.
void set_dio1_pin(InternalGPIOPin *pin)
Set the DIO1 interrupt pin (SX1262).
Definition hub_core.h:119
decisions::ExchangeFirstResponseDisposition wait_for_first_response_(const IoFrame &request, exchange::OutboundExchangeContext &ctx)
Wait loop for the first response packet; classifies via decisions::classify_exchange_first_response.
virtual void queue_request_device_name(const std::string &device_id)
Queue an async device-name request; returns immediately, executed in loop().
PendingOperationType
Type of queued pending operation for the main loop.
Definition hub_core.h:371
@ SET_LOCK_STATE
Queue a set_lock_state call (locked/unlocked).
Definition hub_core.h:375
@ SET_TILT
Queue a set_device_tilt call (tilt percentage 0–100).
Definition hub_core.h:373
@ SET_LIGHT_STATE
Queue a set_light_state call (binary on/off).
Definition hub_core.h:374
@ SET_SWITCH_STATE
Queue a set_switch_state call (binary on/off).
Definition hub_core.h:376
@ REQUEST_NAME
Queue a request_device_name call (poll for stored device name).
Definition hub_core.h:378
@ DISCOVER_AND_PAIR
Queue a discover_and_pair call (starts 3‑phase pairing flow).
Definition hub_core.h:379
@ REQUEST_STATUS
Queue a request_device_status call (poll for current position).
Definition hub_core.h:377
@ SET_POSITION
Queue a set_device_position call (position 0–100 or special values).
Definition hub_core.h:372
void set_radio_type(const std::string &type)
Set radio type ("sx1276" or "sx1262"); empty string means auto‑detect.
Definition hub_core.h:137
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.
void loop() override
Main loop: process pending operations and drive radio state machine.
Definition hub_core.cpp:265
void log_exchange_debug_(const char *device_id) const
Definition hub_core.cpp:59
bool wait_for_key_challenge_(uint32_t timeout_ms, RadioRxPacket &packet, IoFrame &challenge_frame, const uint8_t device_node_id[NODE_ID_SIZE])
Wait for a key-challenge (0x3C) from target device during pairing key exchange.
decisions::ExchangeFinalResponseDisposition wait_for_final_response_(const IoFrame &request, exchange::OutboundExchangeContext &ctx)
Wait loop for the final authenticated response; uses is_valid_final_response().
std::string radio_type_
"sx1276", "sx1262", or "" (auto-detect)
Definition hub_core.h:429
std::vector< DeviceUpdateCallback > callbacks_
Definition hub_core.h:442
void api_rename_device_(const std::string &device_id, const std::string &new_name)
Native API action callback: rename a registered device.
float get_setup_priority() const override
Get setup priority (HARDWARE to initialize early).
Definition hub_core.h:93
bool run_key_exchange_phase_(pairing::PairingContext &context)
Phase 2: authenticated key exchange (0x31 → 0x3C → 0x32 → 0x33).
void set_busy_pin(InternalGPIOPin *pin)
Set the BUSY pin (SX1262).
Definition hub_core.h:121
std::deque< PendingOperation > pending_operations_
Definition hub_core.h:443
virtual bool discover_and_pair()
Discover and pair a device that is in pairing mode.
uint8_t spi_read() override
Read one byte (MISO only).
Definition hub_core.h:109
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:196
void set_dio0_pin(InternalGPIOPin *pin)
Set the DIO0 interrupt pin (SX1276).
Definition hub_core.h:115
decisions::PairingDiscoveryDisposition wait_for_discovery_response_(uint32_t timeout_ms, RadioRxPacket &packet, IoFrame &response_frame)
Wait for a discovery response (0x29) during pairing.
void publish_management_result_(const ManagementActionResult &result)
Publish the outcome of a management action as a Home Assistant event and structured logs.
void set_vfem_pin(InternalGPIOPin *pin)
Set the VFEM power pin.
Definition hub_core.h:125
void update_device_status_(const IoFrame &frame)
Extract supported position or metadata info from a response frame and merge it into the device record...
virtual bool set_light_state(const std::string &device_id, bool on)
Semantic binary helper for light entities.
InternalGPIOPin * dio0_pin_
SX1276 DIO0 interrupt.
Definition hub_core.h:418
void set_system_key(const std::string &key)
Set the system key (hex string).
Definition hub_core.h:131
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:106
void set_tcxo_voltage(uint8_t voltage)
Set TCXO voltage for SX1262 (1.8V / 3.3V).
Definition hub_core.h:139
bool handle_authentication_(const IoFrame &request, uint32_t freq, exchange::OutboundExchangeContext &ctx)
Perform challenge-response (TX auth response) after a 0x3C is received.
void notify_device_update_(const std::string &id)
Fire all registered device update callbacks for the given device ID.
Definition hub_core.cpp:222
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 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:117
virtual bool set_device_position(const std::string &device_id, uint8_t position)
Send a position command to a device.
std::map< std::string, std::vector< std::string > > linked_remotes_
Maps remote node IDs to lists of device IDs they control.
Definition hub_core.h:446
void setup() override
Initialize hardware (radio and device registry).
Definition hub_core.cpp:88
uint8_t spi_transfer(uint8_t data) override
Transfer one byte full‑duplex.
Definition hub_core.h:103
virtual bool request_device_name(const std::string &device_id)
Request the stored device name from a device.
uint8_t tcxo_voltage_
SX1262 TCXO voltage setting (default 1.8 V).
Definition hub_core.h:434
void process_received_packet_(const RadioRxPacket &packet)
Parse a received frame, merge supported device state or metadata, and notify callbacks.
bool authenticate_request_(const IoFrame &request, uint32_t freq)
Handle an inbound authenticated command from a device (status updates, etc.).
void spi_disable() override
Disable the SPI bus.
Definition hub_core.h:99
InternalGPIOPin * busy_pin_
SX1262 BUSY pin.
Definition hub_core.h:421
void register_management_actions_()
Register hub-level Home Assistant actions exposed through ESPHome's native API.
virtual void queue_discover_and_pair()
Queue a pairing operation; executed in loop() when radio idle.
static void parse_device_from_discovery(const IoFrame &frame, IoDevice &device, std::string &device_id)
Parse a discovery response frame into device metadata and ID.
virtual bool request_device_status(const std::string &device_id)
Request current status from a device.
void set_fem_pa_pin(InternalGPIOPin *pin)
Set the FEM PA switch pin.
Definition hub_core.h:127
Button entity that triggers device discovery and pairing when pressed in Home Assistant.
Definition hub_core.h:455
void press_action() override
When button is pressed, queue a discovery/pair operation.
Definition hub_core.h:462
void set_parent(IOHomeControlComponent *parent)
Definition hub_core.h:457
Abstract radio driver for IO-Homecontrol.
Interface for SPI bus access.
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.
PairingDiscoveryDisposition
Disposition during pairing discovery phase.
ExchangeFinalResponseDisposition
Disposition for the final response after authentication.
ExchangeFirstResponseDisposition
Disposition for the first response in an authenticated exchange.
static constexpr float UNKNOWN_POSITION
Sentinel value meaning "position is not known yet".
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
Definition proto_frame.h:81
DeviceType
Device type identifiers reported by IO‑Homecontrol products.
constexpr size_t POSITION_TEXT_BUFFER_SIZE
Buffer for formatted position strings such as "100%".
Definition hub_core.h:52
std::function< void(const std::string &device_id, const IoDevice &device)> DeviceUpdateCallback
Callback type for notifying covers of device state changes.
Definition hub_core.h:47
std::string format_position(float pos)
Format a position float as a human‑readable string (e.g.
Definition hub_core.h:486
bool stored_node_id_is_valid(const uint8_t id[NODE_ID_SIZE])
Check if a stored node ID is valid (not all-zero, not all-0xFF).
Definition hub_core.h:473
constexpr uint8_t DEFAULT_TCXO_VOLTAGE_SETTING_1P8V
SX1262 DIO3 setting value for a 1.8 V TCXO.
Definition hub_core.h:51
static constexpr uint8_t AES_KEY_SIZE
AES-128 key size.
Definition proto_frame.h:84
constexpr uint8_t DEFAULT_PA_PIN_PA_BOOST
SX1276 PA_CONFIG selector for the PA_BOOST output path.
Definition hub_core.h:50
constexpr uint8_t DEFAULT_TX_POWER_DBM
Default TX power used unless YAML overrides it.
Definition hub_core.h:49
IO-Homecontrol 2W protocol definitions.
Radio abstraction layer for IO-Homecontrol.
Debug snapshot of the last exchange attempt.
Definition hub_core.h:390
uint8_t request_cmd
Command ID of the original request (e.g., CMD_EXECUTE=0x00).
Definition hub_core.h:393
uint8_t tries
Try number (1‑based; increments on each retry within EXCHANGE_RETRY_COUNT).
Definition hub_core.h:392
bool capture_rx_done
True if RxDone interrupt fired (packet fully received).
Definition hub_core.h:396
const char * stage
Current stage name (e.g., "TX_REQUEST", "WAIT_FIRST_RESPONSE", "FAILED").
Definition hub_core.h:391
uint8_t capture_frame_len
Length of the parsed protocol frame after recovery/UART decoding.
Definition hub_core.h:402
uint8_t capture_reported_len
Length reported by the radio's packet engine.
Definition hub_core.h:401
uint16_t capture_irq_status
Raw IRQ status register value from the radio chip.
Definition hub_core.h:399
bool capture_crc_error
True if CRC error flagged (SX1262 only; SX1276 IoHomeOn filters in hardware).
Definition hub_core.h:397
uint8_t capture_packet_status
Packet status byte (chip-specific; SX1262 includes CRC flag).
Definition hub_core.h:400
int16_t capture_rssi_dbm
Received signal strength of the captured packet (dBm, negative).
Definition hub_core.h:403
bool capture_valid
True if radio capture data is valid for the last packet seen.
Definition hub_core.h:395
bool saw_challenge
True if a challenge (0x3C) was seen during the exchange.
Definition hub_core.h:394
uint32_t capture_freq_hz
RF frequency of the captured packet (Hz).
Definition hub_core.h:398
Result payload used by hub-level management actions such as rename.
Definition hub_core.h:73
bool has_result_code
True when result_code contains a decoded CMD_ERROR_RESP byte.
Definition hub_core.h:76
bool success
Whether the requested management action succeeded.
Definition hub_core.h:74
std::string requested_name
Requested normalized UTF-8 name for rename actions.
Definition hub_core.h:81
uint8_t result_code
Optional CMD_ERROR_RESP result byte from the device.
Definition hub_core.h:77
std::string device_id
Target IO-homecontrol device ID.
Definition hub_core.h:79
std::string applied_name
Verified cached UTF-8 name after a readback, when available.
Definition hub_core.h:82
std::string action
Action name, for example "rename_device".
Definition hub_core.h:78
bool verified
Whether a follow-up readback verified the applied state.
Definition hub_core.h:75
A single queued operation to be processed in loop().
Definition hub_core.h:383
std::string device_id
Target device ID (hex string, e.g., "123ABC").
Definition hub_core.h:385
PendingOperationType type
Operation type (determines which queue handler to invoke).
Definition hub_core.h:384
uint8_t position
Position/tilt value (0–100) or binary state (ON/UNLOCK=0, OFF/LOCK=100).
Definition hub_core.h:386
Runtime state of a paired IO‑Homecontrol device.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Raw packet received from the radio.
Context carried across one outbound authenticated exchange.
Context object that lives for the duration of a single pairing attempt.
Definition hub_pairing.h:55