[datetime] Fix state_as_esptime() returning invalid timestamp (#15128)

This commit is contained in:
J. Nick Koston
2026-03-24 14:03:56 -10:00
committed by GitHub
parent b6aec4fa25
commit f457b995f7
4 changed files with 71 additions and 8 deletions
@@ -60,6 +60,9 @@ ESPTime DateTimeEntity::state_as_esptime() const {
obj.year = this->year_;
obj.month = this->month_;
obj.day_of_month = this->day_;
obj.day_of_week = 0;
obj.day_of_year = 0;
obj.is_dst = false;
obj.hour = this->hour_;
obj.minute = this->minute_;
obj.second = this->second_;
+1 -1
View File
@@ -231,7 +231,7 @@ void ESPTime::increment_day() {
void ESPTime::recalc_timestamp_utc(bool use_day_of_year) {
time_t res = 0;
if (!this->fields_in_range()) {
if (!this->fields_in_range(false, use_day_of_year)) {
this->timestamp = -1;
return;
}
+13 -5
View File
@@ -79,11 +79,19 @@ struct ESPTime {
/// Check if this ESPTime is valid (all fields in range and year is greater than or equal to 2019)
bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); }
/// Check if all time fields of this ESPTime are in range.
bool fields_in_range() const {
return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 &&
this->day_of_week < 8 && this->day_of_year > 0 && this->day_of_year < 367 && this->month > 0 &&
this->month < 13 && this->day_of_month > 0 && this->day_of_month <= days_in_month(this->month, this->year);
/// Check if time fields are in range.
/// @param check_day_of_week validate day_of_week (not always available when constructing from date/time fields)
/// @param check_day_of_year validate day_of_year (not always available when constructing from date/time fields)
bool fields_in_range(bool check_day_of_week = true, bool check_day_of_year = true) const {
bool valid = this->second < 61 && this->minute < 60 && this->hour < 24 && this->month > 0 && this->month < 13 &&
this->day_of_month > 0 && this->day_of_month <= days_in_month(this->month, this->year);
if (check_day_of_week) {
valid = valid && this->day_of_week > 0 && this->day_of_week < 8;
}
if (check_day_of_year) {
valid = valid && this->day_of_year > 0 && this->day_of_year < 367;
}
return valid;
}
/** Convert a string to ESPTime struct as specified by the format argument.
+54 -2
View File
@@ -1036,8 +1036,6 @@ static time_t esptime_recalc_local(int year, int month, int day, int hour, int m
t.hour = hour;
t.minute = min;
t.second = sec;
t.day_of_week = 1; // Placeholder for fields_in_range()
t.day_of_year = 1;
t.recalc_timestamp_local();
return t.timestamp;
}
@@ -1187,6 +1185,60 @@ TEST(RecalcTimestampLocal, NonDefaultTransitionTime) {
EXPECT_EQ(esp_result, libc_result);
}
TEST(RecalcTimestampLocal, MinimalFieldsWithoutDayOfWeekOrYear) {
// Regression test for issue #15115: DateTimeEntity::state_as_esptime() constructs
// an ESPTime with only year/month/day/hour/minute/second set (no day_of_week or
// day_of_year). recalc_timestamp_local() must work without those fields.
const char *tz_str = "CET-1CEST,M3.5.0,M10.5.0";
setenv("TZ", tz_str, 1);
tzset();
time::ParsedTimezone tz{};
ASSERT_TRUE(parse_posix_tz(tz_str, tz));
set_global_tz(tz);
// Construct ESPTime with only date/time fields (like state_as_esptime does)
ESPTime t{};
t.year = 2026;
t.month = 3;
t.day_of_month = 20;
t.hour = 23;
t.minute = 14;
t.second = 55;
// day_of_week and day_of_year are deliberately left as 0
t.recalc_timestamp_local();
// Must NOT return -1 (the bug: fields_in_range() rejected valid times)
EXPECT_NE(t.timestamp, -1);
// Verify against libc
time_t libc_result = libc_mktime(2026, 3, 20, 23, 14, 55);
EXPECT_EQ(t.timestamp, libc_result);
}
TEST(RecalcTimestampLocal, MinimalFieldsNoDST) {
// Same test but with a timezone that has no DST
const char *tz_str = "IST-5:30";
setenv("TZ", tz_str, 1);
tzset();
time::ParsedTimezone tz{};
ASSERT_TRUE(parse_posix_tz(tz_str, tz));
set_global_tz(tz);
ESPTime t{};
t.year = 2026;
t.month = 3;
t.day_of_month = 23;
t.hour = 10;
t.minute = 0;
t.second = 0;
t.recalc_timestamp_local();
EXPECT_NE(t.timestamp, -1);
time_t libc_result = libc_mktime(2026, 3, 23, 10, 0, 0);
EXPECT_EQ(t.timestamp, libc_result);
}
TEST(RecalcTimestampLocal, YearBoundaryDST) {
// Test southern hemisphere DST across year boundary
// Australia/Sydney: DST active from October to April (spans Jan 1)