Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
periodic_poll_policy.h
Go to the documentation of this file.
1#pragma once
2
3/// @file periodic_poll_policy.h
4/// @brief Schedule and failure bookkeeping for the opt-in periodic polls.
5/// @ingroup hioc_hub
6///
7/// Owns the per-device periodic schedule of one opt-in poll. The hub runs one instance per poll kind:
8/// the limitation read behind `rain_sensor_poll_interval`, the event subscriptions behind
9/// `sun_level_poll_interval` and `battery_level_poll_interval` (ADR 0050, ADR 0064), and the status
10/// read behind `status_refresh_interval` (ADR 0066).
11/// Kept apart from StatusPollPolicy on purpose: that one runs bounded tracking windows which end
12/// when a device stops, this one is an unbounded periodic schedule. Pure logic with injected
13/// timestamps and random numbers — fully host-testable without a clock or a random source.
14
15#include <cstdint>
16#include <map>
17#include <optional>
18#include <string>
19
20namespace esphome {
21namespace home_io_control {
22
23/// Earliest first poll after boot. Sits after the entities' initial status request
24/// (INITIAL_STATUS_REQUEST_DELAY_MS) so the boot burst is spread over time.
25static constexpr uint32_t PERIODIC_POLL_INITIAL_DELAY_MS = 15000;
26/// Width of the random window added to PERIODIC_POLL_INITIAL_DELAY_MS, so devices do not start in step.
27static constexpr uint32_t PERIODIC_POLL_INITIAL_SPREAD_MS = 30000;
28/// Per-poll offset, as a percentage of the interval, applied symmetrically around it. At the
29/// one-minute minimum that is six seconds either way — more than one exchange lasts — so two
30/// devices sharing an interval drift apart within a few polls. Symmetric on purpose: the mean stays
31/// the configured interval, so the transmit time per hour in the documentation stays true.
32static constexpr uint32_t PERIODIC_POLL_JITTER_PERCENT = 10;
33/// Delay before asking again when the device is moving. Keeps the poll out of a running manoeuvre
34/// without waiting a whole interval.
35static constexpr uint32_t PERIODIC_POLL_MOVING_RETRY_MS = 30000;
36/// Delay before asking once more after a poll the device left unanswered (silence, or a challenge
37/// with no final answer). A solar motor misses about one poll in four in the field (issue #156) and
38/// answers a second try within seconds as often as a first, so one quick retry keeps a reading from
39/// going stale over a whole interval. Only an unanswered retry counts as a miss.
40static constexpr uint32_t PERIODIC_POLL_MISS_RETRY_MS = 60000;
41/// Tries per poll. A missed poll only costs one interval of latency, so the full retry budget is
42/// not worth its transmit time here.
43static constexpr uint8_t PERIODIC_POLL_MAX_TRIES = 2;
44/// Consecutive unanswered polls after which the last reading is no longer usable.
45static constexpr uint8_t PERIODIC_POLL_FAILURES_BEFORE_UNKNOWN = 3;
46
47/// @brief Delay of a device's first poll after its interval is set.
48/// @param random A uniformly distributed 32-bit random number.
49/// @return PERIODIC_POLL_INITIAL_DELAY_MS plus an offset in [0, PERIODIC_POLL_INITIAL_SPREAD_MS].
50[[nodiscard]] inline uint32_t periodic_poll_first_delay_ms(uint32_t random) {
52}
53
54/// @brief One poll interval moved by a uniform offset of up to PERIODIC_POLL_JITTER_PERCENT either way.
55///
56/// The offset scales @p random over the whole 32-bit range, so 0 gives the lower bound, UINT32_MAX
57/// the upper bound and the middle of the range the unmodified interval. Computed in 64 bit so a
58/// 24 hour interval cannot overflow.
59/// @param interval_ms The configured interval.
60/// @param random A uniformly distributed 32-bit random number.
61/// @return The delay until the next poll, in [interval - jitter, interval + jitter].
62[[nodiscard]] inline uint32_t periodic_poll_jittered_interval_ms(uint32_t interval_ms, uint32_t random) {
63 const uint64_t jitter = static_cast<uint64_t>(interval_ms) * PERIODIC_POLL_JITTER_PERCENT / 100;
64 const uint64_t offset = (static_cast<uint64_t>(random) * 2 * jitter) / UINT32_MAX; // 0 .. 2 * jitter
65 return static_cast<uint32_t>(interval_ms - jitter + offset);
66}
67
68/// @brief Per-device periodic poll schedule, miss retry and miss counter for one poll kind.
70 public:
71 /// Set the poll interval and schedule the first poll at periodic_poll_first_delay_ms(@p random)
72 /// after @p now. 0 removes the device from the schedule.
73 void set_interval(const std::string &device_id, uint32_t interval_ms, uint32_t now, uint32_t random);
74 /// Return the configured interval (0 when the device is not scheduled).
75 [[nodiscard]] uint32_t get_interval(const std::string &device_id) const;
76
77 /// True when any device's poll is due at @p now. Lets the caller draw a random number only when
78 /// pop_due_device() will use it.
79 [[nodiscard]] bool has_due(uint32_t now) const;
80
81 /// Return one device whose poll is due and immediately schedule its next poll at
82 /// periodic_poll_jittered_interval_ms() after @p now, so a poll that is later dropped from the
83 /// operation queue (pairing flushes background work) is skipped rather than lost forever.
84 /// Returns nullopt when nothing is due. Comparison is wrap-safe across the 32-bit millis() rollover.
85 [[nodiscard]] std::optional<std::string> pop_due_device(uint32_t now, uint32_t random);
86
87 /// Pull the device's next poll forward to @p delay_ms after @p now. Never pushes it later.
88 void defer(const std::string &device_id, uint32_t delay_ms, uint32_t now);
89 /// The device has just given what the poll asks for without being polled: schedule its next poll
90 /// periodic_poll_jittered_interval_ms() after @p now, earlier or later than it was, and drop an
91 /// armed retry. No-op for an unscheduled device.
92 void restart_interval(const std::string &device_id, uint32_t now, uint32_t random);
93
94 /// Count one failed poll (saturating), with no retry: for a poll the device answered with
95 /// something that is not a reading (an error reply, an unexpected shape), which asking again
96 /// soon would not change.
97 /// @return True once PERIODIC_POLL_FAILURES_BEFORE_UNKNOWN is reached, i.e. the last reading is no
98 /// longer usable. Stays true until on_poll_succeeded().
99 bool on_poll_failed(const std::string &device_id);
100 /// Handle a poll the device left unanswered. A scheduled poll arms one retry
101 /// PERIODIC_POLL_MISS_RETRY_MS after @p now (pulled forward like defer()) and counts nothing; the
102 /// retry, if it is left unanswered too, counts one miss like on_poll_failed().
103 /// @return Same as on_poll_failed(): true while the last reading is no longer usable.
104 bool on_poll_unanswered(const std::string &device_id, uint32_t now);
105 /// True while a retry armed by on_poll_unanswered() has not been popped yet.
106 [[nodiscard]] bool retry_armed(const std::string &device_id) const;
107 /// Clear the unanswered-poll count.
108 void on_poll_succeeded(const std::string &device_id);
109 /// True the first time it is called for a device that answered the poll's request with an error
110 /// reply, false afterwards, so the log names that once instead of every poll.
111 [[nodiscard]] bool first_error_reply(const std::string &device_id);
112 /// Consecutive failed polls (0 for an unscheduled device).
113 [[nodiscard]] uint8_t get_failures(const std::string &device_id) const;
114 /// Absolute millis() time of the device's next poll (0 for an unscheduled device).
115 [[nodiscard]] uint32_t get_next_poll(const std::string &device_id) const;
116
117 private:
118 /// Schedule state of one device.
119 struct Entry {
120 uint32_t interval_ms{0}; ///< Configured interval.
121 uint32_t next_poll{0}; ///< Absolute millis() time of the next poll.
122 uint8_t failures{0}; ///< Consecutive failed polls (a retried poll counts once).
123 bool error_reported{false}; ///< True once first_error_reply() has returned true.
124 bool retry_armed{false}; ///< The next pop is the retry of an unanswered poll.
125 bool polling_retry{false}; ///< The poll popped last is such a retry.
126 };
127
128 std::map<std::string, Entry> entries_; ///< Scheduled devices, keyed by device id.
129};
130
131} // namespace home_io_control
132} // namespace esphome
Per-device periodic poll schedule, miss retry and miss counter for one poll kind.
uint32_t get_next_poll(const std::string &device_id) const
Absolute millis() time of the device's next poll (0 for an unscheduled device).
void defer(const std::string &device_id, uint32_t delay_ms, uint32_t now)
Pull the device's next poll forward to delay_ms after now. Never pushes it later.
uint32_t get_interval(const std::string &device_id) const
Return the configured interval (0 when the device is not scheduled).
std::optional< std::string > pop_due_device(uint32_t now, uint32_t random)
Return one device whose poll is due and immediately schedule its next poll at periodic_poll_jittered_...
void restart_interval(const std::string &device_id, uint32_t now, uint32_t random)
The device has just given what the poll asks for without being polled: schedule its next poll periodi...
void on_poll_succeeded(const std::string &device_id)
Clear the unanswered-poll count.
bool on_poll_failed(const std::string &device_id)
Count one failed poll (saturating), with no retry: for a poll the device answered with something that...
bool first_error_reply(const std::string &device_id)
True the first time it is called for a device that answered the poll's request with an error reply,...
uint8_t get_failures(const std::string &device_id) const
Consecutive failed polls (0 for an unscheduled device).
bool has_due(uint32_t now) const
True when any device's poll is due at now.
bool retry_armed(const std::string &device_id) const
True while a retry armed by on_poll_unanswered() has not been popped yet.
bool on_poll_unanswered(const std::string &device_id, uint32_t now)
Handle a poll the device left unanswered.
void set_interval(const std::string &device_id, uint32_t interval_ms, uint32_t now, uint32_t random)
Set the poll interval and schedule the first poll at periodic_poll_first_delay_ms(random) after now.
static constexpr uint32_t PERIODIC_POLL_MOVING_RETRY_MS
Delay before asking again when the device is moving.
static constexpr uint32_t PERIODIC_POLL_MISS_RETRY_MS
Delay before asking once more after a poll the device left unanswered (silence, or a challenge with n...
static constexpr uint32_t PERIODIC_POLL_INITIAL_SPREAD_MS
Width of the random window added to PERIODIC_POLL_INITIAL_DELAY_MS, so devices do not start in step.
static constexpr uint32_t PERIODIC_POLL_JITTER_PERCENT
Per-poll offset, as a percentage of the interval, applied symmetrically around it.
uint32_t periodic_poll_jittered_interval_ms(uint32_t interval_ms, uint32_t random)
One poll interval moved by a uniform offset of up to PERIODIC_POLL_JITTER_PERCENT either way.
static constexpr uint8_t PERIODIC_POLL_MAX_TRIES
Tries per poll.
static constexpr uint32_t PERIODIC_POLL_INITIAL_DELAY_MS
Earliest first poll after boot.
static constexpr uint8_t PERIODIC_POLL_FAILURES_BEFORE_UNKNOWN
Consecutive unanswered polls after which the last reading is no longer usable.
uint32_t periodic_poll_first_delay_ms(uint32_t random)
Delay of a device's first poll after its interval is set.