Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
status_poll_policy.h
Go to the documentation of this file.
1#pragma once
2
3/// @file status_poll_policy.h
4/// @brief Per-device poll scheduling, failure backoff, and follow-up-poll state machine.
5/// @ingroup hioc_hub
6///
7/// Owns all per-device poll bookkeeping (intervals, deadlines, failure counters).
8/// Pure logic with injected timestamps — fully host-testable without a clock mock.
9
10#include <algorithm>
11#include <cstdint>
12#include <map>
13#include <optional>
14#include <string>
15
16namespace esphome {
17namespace home_io_control {
18
19/// @name Background-poll retry delays after silent failures (no reply received)
20/// @{
21static constexpr uint32_t STATUS_RETRY_AFTER_FAIL_MS = 5000; ///< First retry after a silent failure.
22static constexpr uint32_t STATUS_RETRY_AFTER_FAIL_STEP2_MS = 15000; ///< Second retry after a silent failure.
23static constexpr uint32_t STATUS_RETRY_AFTER_FAIL_STEP3_MS = 30000; ///< Third retry after a silent failure.
24static constexpr uint32_t STATUS_RETRY_AFTER_FAIL_STEP4_MS = 60000; ///< Fourth retry after a silent failure.
25static constexpr uint32_t STATUS_RETRY_AFTER_FAIL_MAX_MS =
26 300000; ///< Steady-state backoff after many silent failures.
27/// @}
28
29/// @name Background-poll retry delays after auth-shaped failures (0x3C challenge seen)
30/// @{
31static constexpr uint32_t STATUS_AUTH_RETRY_AFTER_FAIL_MS = 30000; ///< First retry after a challenge-seen failure.
32static constexpr uint32_t STATUS_AUTH_RETRY_AFTER_FAIL_STEP2_MS = 120000; ///< Second retry.
33static constexpr uint32_t STATUS_AUTH_RETRY_AFTER_FAIL_MAX_MS = 300000; ///< Steady-state after repeated auth failures.
34/// @}
35
36/// Hard stop for bounded follow-up polling after a command or remote activity.
37static constexpr uint32_t MAX_TRACKED_STATUS_POLL_WINDOW_MS = 600000;
38/// Delay before the first post-boot status request from an entity.
39static constexpr uint32_t INITIAL_STATUS_REQUEST_DELAY_MS = 5000;
40/// Interval between regular polls of a heater (its `status_poll_interval` overrides it). Slow on
41/// purpose: a heater's state moves over minutes, and a poll costs one exchange per register.
42static constexpr uint32_t HEATING_POLL_DEFAULT_INTERVAL_MS = 300000;
43
44/// @brief Delay of the extra heater poll after a register write the heater acknowledged.
45///
46/// The write is read back at once, but whether the heater now heats shows only in its regulator
47/// register, which the regular poll reads. Without this extra poll the card's heating / idle trails a
48/// setpoint change by up to one poll interval, and longer when a poll fails (issue #165). The
49/// regular cadence restarts from this poll.
50static constexpr uint32_t HEATING_POST_WRITE_POLL_DELAY_MS = 45000;
51/// Delay before polling after overheard remote traffic.
52static constexpr uint32_t REMOTE_ACTIVITY_STATUS_POLL_DELAY_MS = 2000;
53/// Hold queued background polls back for this long after any 1W frame, so a poll the hub itself
54/// scheduled does not occupy the (half-duplex) radio while the remote is still transmitting.
55/// Comfortably longer than the ~160ms reliability burst; short enough to barely shift poll timing
56/// otherwise. Only background polls yield — see decisions::defer_background_poll_for_1w_activity().
57static constexpr uint32_t ONEWAY_QUIET_PERIOD_MS = 700;
58/// Hard cap on how long sustained 1W traffic may hold a background poll back in total, measured
59/// from the start of the burst rather than the most recent frame. ONEWAY_QUIET_PERIOD_MS re-arms on
60/// every frame, so without this cap traffic arriving faster than the quiet period apart — a stuck
61/// remote, a chatty neighbour's sensor — could starve background polls, and therefore device state
62/// freshness, indefinitely. Comfortably above a legitimate ~160ms burst; bounds the worst case
63/// without meaningfully affecting normal operation.
64static constexpr uint32_t ONEWAY_POLL_DEFER_CAP_MS = 5000;
65
66/// Default follow-up settle-poll delay used while a device may still be moving, when no explicit
67/// poll interval is configured (interval_ms == 0). Short on purpose so hint-less devices are
68/// re-checked promptly and the motion-tracking loop terminates soon after the device comes to rest.
69static constexpr uint32_t DEFAULT_SETTLE_POLL_DELAY_MS = 3000;
70/// Upper bound on the settle-poll delay after a STOP command. STOP should confirm the resting
71/// position quickly, so it always settles faster than a normal move, regardless of the configured
72/// interval or device hint.
73static constexpr uint32_t STOP_SETTLE_POLL_CAP_MS = 1000;
74
75/// @brief Resolve the follow-up settle-poll delay while a device may still be moving.
76///
77/// This is the single source of truth for the motion-tracking cadence. The delay is the configured
78/// @p interval_ms, or DEFAULT_SETTLE_POLL_DELAY_MS when no interval is configured (interval_ms == 0).
79/// A device-provided byte-7 hint (@p hint_delay_ms, 0 when absent) and a STOP command
80/// (@p cap_for_stop) may only shorten the delay, never lengthen it.
81///
82/// Note: this resolves the *settle* delay only — it is intentionally independent of whether a
83/// device keeps polling after coming to rest. Periodic monitoring after rest remains opt-in and is
84/// gated on interval_ms != 0 by the command paths, so defaulting the settle delay here does not turn
85/// every device into a periodic poller.
86///
87/// @return Settle delay in milliseconds (always non-zero).
88[[nodiscard]] inline uint32_t settle_delay_ms(uint32_t interval_ms, uint32_t hint_delay_ms, bool cap_for_stop) {
89 uint32_t delay = (interval_ms != 0) ? interval_ms : DEFAULT_SETTLE_POLL_DELAY_MS;
90 if (hint_delay_ms != 0)
91 delay = std::min(delay, hint_delay_ms);
92 if (cap_for_stop)
93 delay = std::min(delay, STOP_SETTLE_POLL_CAP_MS);
94 return delay;
95}
96
97/// @brief Per-device poll scheduling state.
99 uint32_t interval_ms{0}; ///< Configured follow-up poll interval (0 = no configured cadence; delays fall back to
100 ///< device hint / default settle delay).
101 uint32_t next_update{0}; ///< Absolute millis() timestamp for the next poll; 0 = idle.
102 uint32_t poll_deadline{0}; ///< Hard stop for bounded follow-up polling.
103 uint8_t status_poll_failures{0}; ///< Consecutive background poll failures (silent — no reply).
104 uint8_t auth_poll_failures{0}; ///< Consecutive background poll failures that saw a 0x3C challenge.
105 bool stop_settle{false}; ///< The next poll confirms an accepted STOP (see STOP_SETTLE_POLL_TRIES).
106};
107
108/// @brief Per-hub poll scheduling and failure-backoff policy.
109///
110/// Maintains a PollTracking entry per device; all hub poll bookkeeping lives
111/// here and nowhere else. Callers inject 'now' timestamps so the class is
112/// fully testable without a hardware clock.
114 public:
115 // --- Interval configuration ---
116 /// Set the configured follow-up poll interval (ms; 0 = legacy one-shot settle only).
117 void set_interval(const std::string &device_id, uint32_t interval_ms);
118 /// Return the configured interval (0 when not set or unconfigured).
119 [[nodiscard]] uint32_t get_interval(const std::string &device_id) const;
120
121 // --- Tracking lifecycle ---
122 /// Begin bounded follow-up polling after a command or remote activity.
123 /// Sets a deadline regardless of whether a configured interval exists; interval-0 devices use
124 /// device-hinted or default settle delays computed at response time.
125 void begin_tracking(const std::string &device_id, uint32_t initial_delay_ms, uint32_t now);
126 /// Stop all polling for a device and reset failure counts. Preserves interval_ms.
127 void clear(const std::string &device_id);
128
129 // --- Next-update scheduling ---
130 /// Schedule the next poll at an absolute millis() timestamp.
131 void set_next_update(const std::string &device_id, uint32_t abs_time);
132 [[nodiscard]] uint32_t get_next_update(const std::string &device_id) const;
133
134 // --- Failure tracking ---
135 /// Record a failed background poll; apply backoff or clear tracking if the window expired.
136 /// Returns the backoff delay applied in ms, or 0 if tracking was cleared.
137 uint32_t on_exchange_failed(const std::string &device_id, bool auth_like, uint32_t now);
138 /// Reset failure streaks after a successful exchange response.
139 void clear_failure_streaks(const std::string &device_id);
140 [[nodiscard]] uint8_t get_status_poll_failures(const std::string &device_id) const;
141 [[nodiscard]] uint8_t get_auth_poll_failures(const std::string &device_id) const;
142 [[nodiscard]] uint32_t get_poll_deadline(const std::string &device_id) const;
143
144 // --- Post-STOP settle poll ---
145 /// Mark the device's next poll as the one confirming an accepted STOP, so it gets
146 /// STOP_SETTLE_POLL_TRIES. Only a tracked device can be marked; begin_tracking() and clear()
147 /// drop the mark, so a newer command or remote activity never inherits it.
148 void mark_stop_settle(const std::string &device_id);
149 /// Consume the mark set by mark_stop_settle(): true once, for the first poll dispatched after it.
150 [[nodiscard]] bool take_stop_settle(const std::string &device_id);
151
152 // --- Queries ---
153 /// True when the device has an active bounded polling window (deadline set and not expired).
154 [[nodiscard]] bool is_tracking_active(const std::string &device_id, uint32_t now) const;
155
156 // --- Due-device scan ---
157 /// Return and consume the first device whose next_update is overdue.
158 /// Clears expired tracking entries as a side effect. Returns nullopt if nothing is due.
159 [[nodiscard]] std::optional<std::string> pop_due_device(uint32_t now);
160
161 private:
162 std::map<std::string, PollTracking> tracking_;
163
164 /// Compute retry delay from failure count and failure class.
165 static uint32_t retry_delay_ms(uint8_t consecutive_failures, bool auth_like);
166};
167
168} // namespace home_io_control
169} // namespace esphome
Per-hub poll scheduling and failure-backoff policy.
void mark_stop_settle(const std::string &device_id)
Mark the device's next poll as the one confirming an accepted STOP, so it gets STOP_SETTLE_POLL_TRIES...
void set_interval(const std::string &device_id, uint32_t interval_ms)
Set the configured follow-up poll interval (ms; 0 = legacy one-shot settle only).
bool is_tracking_active(const std::string &device_id, uint32_t now) const
True when the device has an active bounded polling window (deadline set and not expired).
uint32_t get_next_update(const std::string &device_id) const
uint32_t get_interval(const std::string &device_id) const
Return the configured interval (0 when not set or unconfigured).
void clear_failure_streaks(const std::string &device_id)
Reset failure streaks after a successful exchange response.
void set_next_update(const std::string &device_id, uint32_t abs_time)
Schedule the next poll at an absolute millis() timestamp.
uint32_t on_exchange_failed(const std::string &device_id, bool auth_like, uint32_t now)
Record a failed background poll; apply backoff or clear tracking if the window expired.
void begin_tracking(const std::string &device_id, uint32_t initial_delay_ms, uint32_t now)
Begin bounded follow-up polling after a command or remote activity.
uint8_t get_status_poll_failures(const std::string &device_id) const
uint8_t get_auth_poll_failures(const std::string &device_id) const
uint32_t get_poll_deadline(const std::string &device_id) const
bool take_stop_settle(const std::string &device_id)
Consume the mark set by mark_stop_settle(): true once, for the first poll dispatched after it.
void clear(const std::string &device_id)
Stop all polling for a device and reset failure counts. Preserves interval_ms.
std::optional< std::string > pop_due_device(uint32_t now)
Return and consume the first device whose next_update is overdue.
static constexpr uint32_t STATUS_RETRY_AFTER_FAIL_STEP4_MS
Fourth retry after a silent failure.
uint32_t settle_delay_ms(uint32_t interval_ms, uint32_t hint_delay_ms, bool cap_for_stop)
Resolve the follow-up settle-poll delay while a device may still be moving.
static constexpr uint32_t ONEWAY_QUIET_PERIOD_MS
Hold queued background polls back for this long after any 1W frame, so a poll the hub itself schedule...
static constexpr uint32_t STATUS_AUTH_RETRY_AFTER_FAIL_MAX_MS
Steady-state after repeated auth failures.
static constexpr uint32_t ONEWAY_POLL_DEFER_CAP_MS
Hard cap on how long sustained 1W traffic may hold a background poll back in total,...
static constexpr uint32_t DEFAULT_SETTLE_POLL_DELAY_MS
Default follow-up settle-poll delay used while a device may still be moving, when no explicit poll in...
static constexpr uint32_t REMOTE_ACTIVITY_STATUS_POLL_DELAY_MS
Delay before polling after overheard remote traffic.
static constexpr uint32_t STATUS_RETRY_AFTER_FAIL_STEP3_MS
Third retry after a silent failure.
static constexpr uint32_t MAX_TRACKED_STATUS_POLL_WINDOW_MS
Hard stop for bounded follow-up polling after a command or remote activity.
static constexpr uint32_t HEATING_POST_WRITE_POLL_DELAY_MS
Delay of the extra heater poll after a register write the heater acknowledged.
static constexpr uint32_t STATUS_RETRY_AFTER_FAIL_MS
First retry after a silent failure.
static constexpr uint32_t STATUS_RETRY_AFTER_FAIL_MAX_MS
Steady-state backoff after many silent failures.
static constexpr uint32_t INITIAL_STATUS_REQUEST_DELAY_MS
Delay before the first post-boot status request from an entity.
static constexpr uint32_t STATUS_AUTH_RETRY_AFTER_FAIL_STEP2_MS
Second retry.
static constexpr uint32_t STATUS_AUTH_RETRY_AFTER_FAIL_MS
First retry after a challenge-seen failure.
static constexpr uint32_t STOP_SETTLE_POLL_CAP_MS
Upper bound on the settle-poll delay after a STOP command.
static constexpr uint32_t STATUS_RETRY_AFTER_FAIL_STEP2_MS
Second retry after a silent failure.
static constexpr uint32_t HEATING_POLL_DEFAULT_INTERVAL_MS
Interval between regular polls of a heater (its status_poll_interval overrides it).
Per-device poll scheduling state.
uint8_t auth_poll_failures
Consecutive background poll failures that saw a 0x3C challenge.
bool stop_settle
The next poll confirms an accepted STOP (see STOP_SETTLE_POLL_TRIES).
uint8_t status_poll_failures
Consecutive background poll failures (silent — no reply).
uint32_t poll_deadline
Hard stop for bounded follow-up polling.
uint32_t interval_ms
Configured follow-up poll interval (0 = no configured cadence; delays fall back to device hint / defa...
uint32_t next_update
Absolute millis() timestamp for the next poll; 0 = idle.