diff --git a/esphome/components/time/posix_tz.cpp b/esphome/components/time/posix_tz.cpp index d400cce454..315216b09c 100644 --- a/esphome/components/time/posix_tz.cpp +++ b/esphome/components/time/posix_tz.cpp @@ -3,6 +3,13 @@ namespace esphome::time { +// Global timezone for ESPTime::from_epoch_local() to use +static ParsedTimezone global_tz_{}; + +void set_global_tz(const ParsedTimezone &tz) { global_tz_ = tz; } + +const ParsedTimezone &get_global_tz() { return global_tz_; } + namespace internal { // Helper to parse an unsigned integer from string, updating pointer diff --git a/esphome/components/time/posix_tz.h b/esphome/components/time/posix_tz.h index ea9864b304..27e51e7519 100644 --- a/esphome/components/time/posix_tz.h +++ b/esphome/components/time/posix_tz.h @@ -53,6 +53,14 @@ bool parse_posix_tz(const char *tz_string, ParsedTimezone &result); /// @return true on success bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm); +/// Set the global timezone used by epoch_to_local_tm() when called without a timezone. +/// This is called by RealTimeClock::apply_timezone_() to enable ESPTime::from_epoch_local() +/// to work without libc's localtime(). +void set_global_tz(const ParsedTimezone &tz); + +/// Get the global timezone. +const ParsedTimezone &get_global_tz(); + // Internal helper functions exposed for testing namespace internal { diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 4792746520..84d611d423 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -25,17 +25,16 @@ RealTimeClock::RealTimeClock() = default; void RealTimeClock::dump_config() { #ifdef USE_TIME_TIMEZONE - int std_hours = -this->parsed_tz_.std_offset_seconds / 3600; - int std_mins = abs(this->parsed_tz_.std_offset_seconds % 3600) / 60; + const auto &tz = get_global_tz(); + int std_hours = -tz.std_offset_seconds / 3600; + int std_mins = abs(tz.std_offset_seconds % 3600) / 60; ESP_LOGCONFIG(TAG, "Timezone: UTC%+d:%02d", std_hours, std_mins); - if (this->parsed_tz_.has_dst) { - int dst_hours = -this->parsed_tz_.dst_offset_seconds / 3600; + if (tz.has_dst) { + int dst_hours = -tz.dst_offset_seconds / 3600; // Always use M format - tzdata and aioesphomeapi only generate M format rules - ESP_LOGCONFIG(TAG, " DST: UTC%+d, M%d.%d.%d/%" PRId32 " - M%d.%d.%d/%" PRId32, dst_hours, - this->parsed_tz_.dst_start.month, this->parsed_tz_.dst_start.week, - this->parsed_tz_.dst_start.day_of_week, this->parsed_tz_.dst_start.time_seconds / 3600, - this->parsed_tz_.dst_end.month, this->parsed_tz_.dst_end.week, this->parsed_tz_.dst_end.day_of_week, - this->parsed_tz_.dst_end.time_seconds / 3600); + ESP_LOGCONFIG(TAG, " DST: UTC%+d, M%d.%d.%d/%" PRId32 " - M%d.%d.%d/%" PRId32, dst_hours, tz.dst_start.month, + tz.dst_start.week, tz.dst_start.day_of_week, tz.dst_start.time_seconds / 3600, tz.dst_end.month, + tz.dst_end.week, tz.dst_end.day_of_week, tz.dst_end.time_seconds / 3600); } #endif auto time = this->now(); @@ -96,24 +95,23 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { #ifdef USE_TIME_TIMEZONE void RealTimeClock::apply_timezone_(const char *tz) { + ParsedTimezone parsed{}; + // Handle null input if (tz == nullptr) { ESP_LOGW(TAG, "Failed to parse timezone: (null)"); - this->parsed_tz_ = ParsedTimezone{}; + set_global_tz(parsed); return; } - // Set TZ env var for components using libc's localtime() directly - // (e.g., sun, datetime, wireguard, deep_sleep) - setenv("TZ", tz, 1); - tzset(); - - // Parse the POSIX TZ string using our custom parser for RealTimeClock::now() - if (!parse_posix_tz(tz, this->parsed_tz_)) { + // Parse the POSIX TZ string using our custom parser + if (!parse_posix_tz(tz, parsed)) { ESP_LOGW(TAG, "Failed to parse timezone: %s", tz); - // Reset to UTC on parse failure - this->parsed_tz_ = ParsedTimezone{}; + // parsed stays as default (UTC) on failure } + + // Set global timezone for all time conversions + set_global_tz(parsed); } #endif diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 6e6d13f17f..78e99a1924 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -47,7 +47,7 @@ class RealTimeClock : public PollingComponent { #ifdef USE_TIME_TIMEZONE time_t epoch = this->timestamp_now(); struct tm local_tm; - if (epoch_to_local_tm(epoch, this->parsed_tz_, &local_tm)) { + if (epoch_to_local_tm(epoch, get_global_tz(), &local_tm)) { return ESPTime::from_c_tm(&local_tm, epoch); } // Fallback to UTC if parsing failed @@ -74,7 +74,6 @@ class RealTimeClock : public PollingComponent { void synchronize_epoch_(uint32_t epoch); #ifdef USE_TIME_TIMEZONE - ParsedTimezone parsed_tz_{}; void apply_timezone_(const char *tz); #endif diff --git a/esphome/core/time.h b/esphome/core/time.h index 87ebb5c221..718477563a 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -7,6 +7,10 @@ #include #include +#ifdef USE_TIME_TIMEZONE +#include "esphome/components/time/posix_tz.h" +#endif + namespace esphome { template bool increment_time_value(T ¤t, uint16_t begin, uint16_t end); @@ -105,11 +109,20 @@ struct ESPTime { * @return The generated ESPTime */ static ESPTime from_epoch_local(time_t epoch) { +#ifdef USE_TIME_TIMEZONE + struct tm local_tm; + if (time::epoch_to_local_tm(epoch, time::get_global_tz(), &local_tm)) { + return ESPTime::from_c_tm(&local_tm, epoch); + } + // Fallback to UTC if conversion failed + return ESPTime::from_epoch_utc(epoch); +#else struct tm *c_tm = ::localtime(&epoch); if (c_tm == nullptr) { return ESPTime{}; // Return an invalid ESPTime } return ESPTime::from_c_tm(c_tm, epoch); +#endif } /** Convert an UTC epoch timestamp to a UTC time ESPTime instance. *