mirror of
https://github.com/esphome/esphome.git
synced 2026-05-26 19:26:25 +08:00
[datetime] Fix state_as_esptime() returning invalid timestamp (#15128)
This commit is contained in:
@@ -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_;
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user