[uptime] Use scheduler millis_64() for rollover-safe uptime tracking (#14170)

This commit is contained in:
J. Nick Koston
2026-02-20 21:08:49 -06:00
committed by GitHub
parent 8589f80d8f
commit abe37c9841
8 changed files with 24 additions and 61 deletions

View File

@@ -1,30 +1,16 @@
#include "uptime_seconds_sensor.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome {
namespace uptime {
namespace esphome::uptime {
static const char *const TAG = "uptime.sensor";
void UptimeSecondsSensor::update() {
const uint32_t ms = millis();
const uint64_t ms_mask = (1ULL << 32) - 1ULL;
const uint32_t last_ms = this->uptime_ & ms_mask;
if (ms < last_ms) {
this->uptime_ += ms_mask + 1ULL;
ESP_LOGD(TAG, "Detected roll-over \xf0\x9f\xa6\x84");
}
this->uptime_ &= ~ms_mask;
this->uptime_ |= ms;
// Do separate second and milliseconds conversion to avoid floating point division errors
// Probably some IEEE standard already guarantees this division can be done without loss
// of precision in a single division, but let's do it like this to be sure.
const uint64_t seconds_int = this->uptime_ / 1000ULL;
const float seconds = float(seconds_int) + (this->uptime_ % 1000ULL) / 1000.0f;
const uint64_t uptime = App.scheduler.millis_64();
const uint64_t seconds_int = uptime / 1000ULL;
const float seconds = float(seconds_int) + (uptime % 1000ULL) / 1000.0f;
this->publish_state(seconds);
}
float UptimeSecondsSensor::get_setup_priority() const { return setup_priority::HARDWARE; }
@@ -33,5 +19,4 @@ void UptimeSecondsSensor::dump_config() {
ESP_LOGCONFIG(TAG, " Type: Seconds");
}
} // namespace uptime
} // namespace esphome
} // namespace esphome::uptime

View File

@@ -3,8 +3,7 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
namespace esphome {
namespace uptime {
namespace esphome::uptime {
class UptimeSecondsSensor : public sensor::Sensor, public PollingComponent {
public:
@@ -12,10 +11,6 @@ class UptimeSecondsSensor : public sensor::Sensor, public PollingComponent {
void dump_config() override;
float get_setup_priority() const override;
protected:
uint64_t uptime_{0};
};
} // namespace uptime
} // namespace esphome
} // namespace esphome::uptime

View File

@@ -6,8 +6,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace uptime {
namespace esphome::uptime {
static const char *const TAG = "uptime.sensor";
@@ -33,7 +32,6 @@ void UptimeTimestampSensor::dump_config() {
ESP_LOGCONFIG(TAG, " Type: Timestamp");
}
} // namespace uptime
} // namespace esphome
} // namespace esphome::uptime
#endif // USE_TIME

View File

@@ -8,8 +8,7 @@
#include "esphome/components/time/real_time_clock.h"
#include "esphome/core/component.h"
namespace esphome {
namespace uptime {
namespace esphome::uptime {
class UptimeTimestampSensor : public sensor::Sensor, public Component {
public:
@@ -24,7 +23,6 @@ class UptimeTimestampSensor : public sensor::Sensor, public Component {
time::RealTimeClock *time_;
};
} // namespace uptime
} // namespace esphome
} // namespace esphome::uptime
#endif // USE_TIME

View File

@@ -1,11 +1,10 @@
#include "uptime_text_sensor.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace uptime {
namespace esphome::uptime {
static const char *const TAG = "uptime.sensor";
@@ -17,22 +16,10 @@ static void append_unit(char *buf, size_t buf_size, size_t &pos, const char *sep
pos = buf_append_printf(buf, buf_size, pos, "%u%s", value, label);
}
void UptimeTextSensor::setup() {
this->last_ms_ = millis();
if (this->last_ms_ < 60 * 1000)
this->last_ms_ = 0;
this->update();
}
void UptimeTextSensor::setup() { this->update(); }
void UptimeTextSensor::update() {
auto now = millis();
// get whole seconds since last update. Note that even if the millis count has overflowed between updates,
// the difference will still be correct due to the way twos-complement arithmetic works.
uint32_t delta = now - this->last_ms_;
this->last_ms_ = now - delta % 1000; // save remainder for next update
delta /= 1000;
this->uptime_ += delta;
uint32_t uptime = this->uptime_;
uint32_t uptime = static_cast<uint32_t>(App.scheduler.millis_64() / 1000);
unsigned interval = this->get_update_interval() / 1000;
// Calculate all time units
@@ -89,5 +76,4 @@ void UptimeTextSensor::update() {
float UptimeTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; }
void UptimeTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Uptime Text Sensor", this); }
} // namespace uptime
} // namespace esphome
} // namespace esphome::uptime

View File

@@ -5,8 +5,7 @@
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/core/component.h"
namespace esphome {
namespace uptime {
namespace esphome::uptime {
class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent {
public:
@@ -35,9 +34,6 @@ class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent
const char *seconds_text_;
const char *separator_;
bool expand_{};
uint32_t uptime_{0}; // uptime in seconds, will overflow after 136 years
uint32_t last_ms_{0};
};
} // namespace uptime
} // namespace esphome
} // namespace esphome::uptime

View File

@@ -675,6 +675,8 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type
return total_cancelled > 0;
}
uint64_t Scheduler::millis_64() { return this->millis_64_(millis()); }
uint64_t Scheduler::millis_64_(uint32_t now) {
// THREAD SAFETY NOTE:
// This function has three implementations, based on the precompiler flags

View File

@@ -116,6 +116,9 @@ class Scheduler {
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
bool cancel_retry(Component *component, uint32_t id);
/// Get 64-bit millisecond timestamp (handles 32-bit millis() rollover)
uint64_t millis_64();
// Calculate when the next scheduled item should run
// @param now Fresh timestamp from millis() - must not be stale/cached
// Returns the time in milliseconds until the next scheduled item, or nullopt if no items