Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
operation_queue.h
Go to the documentation of this file.
1#pragma once
2
3/// @file operation_queue.h
4/// @brief Pending-operation queue with per-type coalescing and deduplication.
5/// @ingroup hioc_hub
6///
7/// Owns the PendingOperationType / PendingOperation types and the coalescing
8/// rules applied when operations are enqueued. Pure data and logic — no ESPHome
9/// dependencies, fully host-testable.
10
11#include "proto_device_model.h"
12
13#include <cstdint>
14#include <deque>
15#include <optional>
16#include <string>
17
18namespace esphome {
19namespace home_io_control {
20
21/// Position value written for binary ON commands (light on, switch on, lock unlock).
22static constexpr uint8_t BINARY_ENTITY_ON_POSITION = 0;
23/// Position value written for binary OFF commands (light off, switch off, lock lock).
24static constexpr uint8_t BINARY_ENTITY_OFF_POSITION = 100;
25
26/// @brief Discriminator for entries in the pending-operation deque.
27enum class PendingOperationType : uint8_t {
28 SET_POSITION, ///< set_device_position call (position 0–100 or special values).
29 SET_TILT, ///< set_device_tilt call (tilt percentage 0–100).
30 SET_POSITION_AND_TILT, ///< Combined set_device_position_and_tilt call.
31 DEVICE_COMMAND, ///< Named device command (STOP, FAVORITE, VENT).
32 SET_LIGHT_STATE, ///< set_light_state call (binary on/off).
33 SET_LOCK_STATE, ///< set_lock_state call (locked/unlocked).
34 SET_SWITCH_STATE, ///< set_switch_state call (binary on/off).
35 SET_FAN_POSITION, ///< set_fan_position call (heat level of an exterior heater as an IO position).
36 ONEWAY_COMMAND, ///< 1W named command sent as a controller identity.
37 ONEWAY_POSITION, ///< 1W numeric position sent as a controller identity.
38 ONEWAY_ENROLL, ///< 1W add-controller (0x30) registering an identity.
39 ONEWAY_UNENROLL, ///< 1W remove-controller (0x39) un-registering an identity.
40 REQUEST_STATUS, ///< request_device_status call (poll for current position).
41 REQUEST_NAME, ///< request_device_name call (poll for stored device name).
42 REQUEST_LIMITATION, ///< request_device_limitation call (read the device's minimum limitation).
43 REQUEST_SUN_LEVEL, ///< request_device_sun_level call (subscribe to EVENT_LOCAL_SENSOR, read the answer).
44 REQUEST_BATTERY_LEVEL, ///< request_device_battery_level call (subscribe to EVENT_BATTERY_LEVEL_CHANGE, read the
45 ///< answer).
46 DISCOVER_AND_PAIR, ///< discover_and_pair call (starts 3-phase pairing flow).
47};
48
49/// @brief A single queued operation to be dispatched from loop().
51 PendingOperationType type; ///< Operation type (determines which handler to invoke).
52 /// Target device ID (hex string, e.g., "123ABC") — **except** for the ONEWAY_* types, where it
53 /// carries the controller-identity handle instead. 1W addresses a device class, not a node, so
54 /// there is no device ID to put here; the identity is what the operation is bound to. The field
55 /// is reused rather than duplicated because every queue entry would otherwise grow a second
56 /// string for the benefit of two operation types.
57 std::string device_id;
58 uint8_t position{0}; ///< Position/tilt value (0–100) or binary state (ON=0, OFF=100).
59 uint8_t tilt{0}; ///< Tilt value for SET_POSITION_AND_TILT (0–100).
60 CoverCommand command{CoverCommand::STOP}; ///< Named command for DEVICE_COMMAND operations.
61};
62
63/// @brief Serialized pending-operation queue with coalescing, deduplication, and two-band ordering.
64///
65/// **Ordering:** DISCOVER_AND_PAIR > control operations > background polls.
66/// Control operations (SET_*, DEVICE_COMMAND) are inserted before any queued background polls
67/// (REQUEST_STATUS, REQUEST_NAME, REQUEST_LIMITATION, REQUEST_SUN_LEVEL, REQUEST_BATTERY_LEVEL) so user-visible
68/// commands execute with minimum queue latency.
69/// DISCOVER_AND_PAIR always front-inserts ahead of all other entries.
70///
71/// **Stale-poll drop:** enqueueing a control operation for device X drops any queued
72/// REQUEST_STATUS for X, since the command reply supersedes it and re-arms tracking. A queued
73/// REQUEST_LIMITATION, REQUEST_SUN_LEVEL or REQUEST_BATTERY_LEVEL is kept: a status reply does not answer it.
74///
75/// Coalescing rules (both directions):
76/// - SET_POSITION arriving while SET_TILT is pending for the same device → SET_POSITION_AND_TILT.
77/// - SET_TILT arriving while SET_POSITION is pending for the same device → SET_POSITION_AND_TILT.
78///
79/// Deduplication rules:
80/// - At most one REQUEST_STATUS per device.
81/// - At most one REQUEST_NAME per device.
82/// - At most one REQUEST_LIMITATION, one REQUEST_SUN_LEVEL and one REQUEST_BATTERY_LEVEL per device.
83/// - At most one DISCOVER_AND_PAIR (every pending background poll is flushed
84/// and the discover entry is pushed to the front to minimize pairing-window latency).
85///
86/// All coalescing and deduplication log messages are suppressed here — callers are responsible
87/// for emitting them because OperationQueue must remain free of ESPHome logging dependencies.
89 public:
90 // --- Position / tilt (with coalescing) ---
91
92 /// Enqueue SET_POSITION, or coalesce with a pending SET_TILT into SET_POSITION_AND_TILT.
93 /// @return true if coalesced (caller should log the merge), false if a new entry was pushed.
94 bool enqueue_set_position(const std::string &device_id, uint8_t position);
95
96 /// Enqueue SET_TILT, or coalesce with a pending SET_POSITION into SET_POSITION_AND_TILT.
97 /// @return true if coalesced (caller should log the merge), false if a new entry was pushed.
98 bool enqueue_set_tilt(const std::string &device_id, uint8_t tilt_percent);
99
100 /// Enqueue SET_POSITION_AND_TILT directly (no coalescing needed).
101 void enqueue_set_position_and_tilt(const std::string &device_id, uint8_t position, uint8_t tilt_percent);
102
103 // --- Named cover command ---
104 void enqueue_device_command(const std::string &device_id, CoverCommand cmd);
105
106 // --- Binary entity commands (no coalescing) ---
107 /// Enqueue SET_LIGHT_STATE with an arbitrary IO position (0-100), for dimmable lights.
108 /// enqueue_set_light_state() is a thin binary-position wrapper around this.
109 void enqueue_set_light_position(const std::string &device_id, uint8_t position);
110 void enqueue_set_light_state(const std::string &device_id, bool on);
111 /// Enqueue SET_FAN_POSITION with an IO position (0-100; 0 = full heat, 100 = off).
112 void enqueue_set_fan_position(const std::string &device_id, uint8_t position);
113 void enqueue_set_lock_state(const std::string &device_id, bool locked);
114 void enqueue_set_switch_state(const std::string &device_id, bool on);
115
116 // --- 1W commands (no coalescing, no deduplication) ---
117
118 /// Enqueue a 1W named command for a controller identity.
119 ///
120 /// Deliberately neither coalesced nor deduplicated, unlike their 2W counterparts. A 2W command
121 /// can be superseded because the device reports back what it did; a 1W command cannot be
122 /// confirmed at all, so dropping one is dropping a press the user made with nothing to notice
123 /// it. Each press also consumes its own sequence, and merging two would leave a gap that looks
124 /// like a lost frame to a device tracking the counter.
125 /// @param controller_id Controller-identity handle (see PendingOperation::device_id).
126 /// @param cmd Named command to send.
127 void enqueue_oneway_command(const std::string &controller_id, CoverCommand cmd);
128
129 /// Enqueue a 1W numeric position for a controller identity.
130 /// @param controller_id Controller-identity handle (see PendingOperation::device_id).
131 /// @param position Target position 0–100.
132 void enqueue_oneway_position(const std::string &controller_id, uint8_t position);
133
134 /// Enqueue a 1W enrollment (add-controller) for a controller identity. Same no-coalesce,
135 /// no-dedup contract as the other 1W ops, for the same reason.
136 /// @param controller_id Controller-identity handle (see PendingOperation::device_id).
137 void enqueue_oneway_enroll(const std::string &controller_id);
138
139 /// Enqueue a 1W un-enrollment (remove-controller) for a controller identity.
140 /// @param controller_id Controller-identity handle (see PendingOperation::device_id).
141 void enqueue_oneway_unenroll(const std::string &controller_id);
142
143 // --- Background polls (with deduplication) ---
144
145 /// Enqueue REQUEST_STATUS, suppressing duplicates.
146 /// @return true if pushed, false if a duplicate was already pending.
147 bool enqueue_request_status(const std::string &device_id);
148
149 /// Enqueue REQUEST_NAME, suppressing duplicates.
150 /// @return true if pushed, false if a duplicate was already pending.
151 bool enqueue_request_name(const std::string &device_id);
152
153 /// Enqueue REQUEST_LIMITATION, suppressing duplicates.
154 /// @return true if pushed, false if a duplicate was already pending.
155 bool enqueue_request_limitation(const std::string &device_id);
156
157 /// Enqueue REQUEST_SUN_LEVEL, suppressing duplicates.
158 /// @return true if pushed, false if a duplicate was already pending.
159 bool enqueue_request_sun_level(const std::string &device_id);
160
161 /// Enqueue REQUEST_BATTERY_LEVEL, suppressing duplicates.
162 /// @return true if pushed, false if a duplicate was already pending.
163 bool enqueue_request_battery_level(const std::string &device_id);
164
165 // --- Pairing (dedup + priority front-push) ---
166
167 /// Enqueue DISCOVER_AND_PAIR with elevated priority.
168 /// Flushes every pending background poll (see is_background_op()) and pushes to front.
169 /// @return true if pushed, false if a DISCOVER_AND_PAIR was already pending.
171
172 // --- Queue access ---
173 [[nodiscard]] std::optional<PendingOperation> pop();
174 [[nodiscard]] bool empty() const;
175 [[nodiscard]] std::size_t size() const;
176
177 // --- Read-only inspection ---
178 [[nodiscard]] const PendingOperation &front() const;
179 [[nodiscard]] const PendingOperation &back() const;
180 [[nodiscard]] const PendingOperation &operator[](std::size_t index) const;
181 [[nodiscard]] std::deque<PendingOperation>::const_iterator begin() const;
182 [[nodiscard]] std::deque<PendingOperation>::const_iterator end() const;
183
184 /// True for background poll types (REQUEST_STATUS, REQUEST_NAME and the sensor polls) that yield to control
185 /// operations. Public because the dispatch gate in loop() defers *only* background work when a 1W remote is
186 /// transmitting; a user command must never be held back for it.
187 [[nodiscard]] static bool is_background_op(PendingOperationType t);
188
189 private:
190 std::deque<PendingOperation> queue_;
191 /// Insert a control operation before the first background entry; also drops any queued
192 /// REQUEST_STATUS for the same device since the incoming command supersedes it.
193 void push_control_(PendingOperation op);
194 /// Append a background operation of @p type for @p device_id unless an identical one is already
195 /// queued. @return true if pushed, false if a duplicate was already pending.
196 bool enqueue_background_unique_(PendingOperationType type, const std::string &device_id);
197};
198
199} // namespace home_io_control
200} // namespace esphome
Serialized pending-operation queue with coalescing, deduplication, and two-band ordering.
std::deque< PendingOperation >::const_iterator begin() const
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_set_lock_state(const std::string &device_id, bool locked)
void enqueue_set_position_and_tilt(const std::string &device_id, uint8_t position, uint8_t tilt_percent)
Enqueue SET_POSITION_AND_TILT directly (no coalescing needed).
void enqueue_oneway_command(const std::string &controller_id, CoverCommand cmd)
Enqueue a 1W named command for a controller identity.
std::optional< PendingOperation > pop()
void enqueue_device_command(const std::string &device_id, CoverCommand cmd)
bool enqueue_request_status(const std::string &device_id)
Enqueue REQUEST_STATUS, suppressing duplicates.
bool enqueue_request_name(const std::string &device_id)
Enqueue REQUEST_NAME, suppressing duplicates.
bool enqueue_request_sun_level(const std::string &device_id)
Enqueue REQUEST_SUN_LEVEL, suppressing duplicates.
bool enqueue_request_limitation(const std::string &device_id)
Enqueue REQUEST_LIMITATION, suppressing duplicates.
void enqueue_set_fan_position(const std::string &device_id, uint8_t position)
Enqueue SET_FAN_POSITION with an IO position (0-100; 0 = full heat, 100 = off).
void enqueue_set_switch_state(const std::string &device_id, bool on)
void enqueue_oneway_position(const std::string &controller_id, uint8_t position)
Enqueue a 1W numeric position for a controller identity.
bool enqueue_discover_and_pair()
Enqueue DISCOVER_AND_PAIR with elevated priority.
static bool is_background_op(PendingOperationType t)
True for background poll types (REQUEST_STATUS, REQUEST_NAME and the sensor polls) that yield to cont...
std::deque< PendingOperation >::const_iterator end() const
bool enqueue_set_position(const std::string &device_id, uint8_t position)
Enqueue SET_POSITION, or coalesce with a pending SET_TILT into SET_POSITION_AND_TILT.
const PendingOperation & back() const
bool enqueue_request_battery_level(const std::string &device_id)
Enqueue REQUEST_BATTERY_LEVEL, suppressing duplicates.
void enqueue_oneway_enroll(const std::string &controller_id)
Enqueue a 1W enrollment (add-controller) for a controller identity.
void enqueue_set_light_state(const std::string &device_id, bool on)
void enqueue_set_light_position(const std::string &device_id, uint8_t position)
Enqueue SET_LIGHT_STATE with an arbitrary IO position (0-100), for dimmable lights.
const PendingOperation & operator[](std::size_t index) const
bool enqueue_set_tilt(const std::string &device_id, uint8_t tilt_percent)
Enqueue SET_TILT, or coalesce with a pending SET_POSITION into SET_POSITION_AND_TILT.
PendingOperationType
Discriminator for entries in the pending-operation deque.
@ SET_LOCK_STATE
set_lock_state call (locked/unlocked).
@ SET_TILT
set_device_tilt call (tilt percentage 0–100).
@ SET_POSITION_AND_TILT
Combined set_device_position_and_tilt call.
@ SET_LIGHT_STATE
set_light_state call (binary on/off).
@ SET_FAN_POSITION
set_fan_position call (heat level of an exterior heater as an IO position).
@ REQUEST_BATTERY_LEVEL
request_device_battery_level call (subscribe to EVENT_BATTERY_LEVEL_CHANGE, read the answer).
@ SET_SWITCH_STATE
set_switch_state call (binary on/off).
@ REQUEST_NAME
request_device_name call (poll for stored device name).
@ ONEWAY_COMMAND
1W named command sent as a controller identity.
@ REQUEST_SUN_LEVEL
request_device_sun_level call (subscribe to EVENT_LOCAL_SENSOR, read the answer).
@ DISCOVER_AND_PAIR
discover_and_pair call (starts 3-phase pairing flow).
@ REQUEST_STATUS
request_device_status call (poll for current position).
@ ONEWAY_UNENROLL
1W remove-controller (0x39) un-registering an identity.
@ DEVICE_COMMAND
Named device command (STOP, FAVORITE, VENT).
@ SET_POSITION
set_device_position call (position 0–100 or special values).
@ REQUEST_LIMITATION
request_device_limitation call (read the device's minimum limitation).
@ ONEWAY_ENROLL
1W add-controller (0x30) registering an identity.
@ ONEWAY_POSITION
1W numeric position sent as a controller identity.
CoverCommand
Named device commands for cover-type actuators.
static constexpr uint8_t BINARY_ENTITY_ON_POSITION
Position value written for binary ON commands (light on, switch on, lock unlock).
static constexpr uint8_t BINARY_ENTITY_OFF_POSITION
Position value written for binary OFF commands (light off, switch off, lock lock).
IO-Homecontrol device-type model, capabilities and runtime device state.
A single queued operation to be dispatched from loop().
CoverCommand command
Named command for DEVICE_COMMAND operations.
std::string device_id
Target device ID (hex string, e.g., "123ABC") — except for the ONEWAY_* types, where it carries the c...
uint8_t tilt
Tilt value for SET_POSITION_AND_TILT (0–100).
uint8_t position
Position/tilt value (0–100) or binary state (ON=0, OFF=100).
PendingOperationType type
Operation type (determines which handler to invoke).