[time] Fix strftime %Z and %z returning wrong timezone (#15330)

This commit is contained in:
J. Nick Koston
2026-03-31 12:59:45 -10:00
committed by GitHub
parent 9dca7e0daf
commit 23dcc5389d
3 changed files with 68 additions and 2 deletions
+13
View File
@@ -4,6 +4,7 @@
#include "posix_tz.h"
#include <cctype>
#include <cstdio>
namespace esphome::time {
@@ -442,6 +443,18 @@ bool parse_posix_tz(const char *tz_string, ParsedTimezone &result) {
return internal::parse_dst_rule(p, result.dst_end);
}
// Format a POSIX offset (positive = west) as "+HHMM" / "-HHMM" for display.
// Convention: negate POSIX sign so east-of-UTC is positive (ISO 8601 / RFC 2822).
void format_designation(int32_t posix_offset, char *buf, size_t buf_size) {
int32_t display = -posix_offset;
char sign = display >= 0 ? '+' : '-';
if (display < 0)
display = -display;
int h = display / 3600;
int m = (display % 3600) / 60;
snprintf(buf, buf_size, "%c%02d%02d", sign, h, m);
}
bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm) {
if (!out_tm) {
return false;
+3
View File
@@ -36,6 +36,9 @@ struct ParsedTimezone {
bool has_dst() const { return this->dst_start.type != DSTRuleType::NONE; }
};
/// Format a POSIX offset as "+HHMM"/"-HHMM" into buf (must be >= 6 bytes).
void format_designation(int32_t posix_offset, char *buf, size_t buf_size);
/// Parse a POSIX TZ string into a ParsedTimezone struct.
///
/// @deprecated Remove before 2026.9.0 (bridge code for backward compatibility).
+52 -2
View File
@@ -2,6 +2,9 @@
#include "helpers.h"
#include <algorithm>
#ifdef USE_TIME_TIMEZONE
#include "esphome/components/time/posix_tz.h"
#endif
namespace esphome {
@@ -14,12 +17,59 @@ uint8_t days_in_month(uint8_t month, uint16_t year) {
size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) {
struct tm c_tm = this->to_c_tm();
#ifdef USE_TIME_TIMEZONE
// ::strftime uses libc's internal timezone state for %Z and %z, but we
// eliminated setenv("TZ")/tzset() on embedded platforms to save flash.
// Substitute %Z and %z with correct values from our parsed timezone.
// Quick scan: does format contain %Z or %z (but not %%Z/%%z)?
bool needs_subst = false;
for (const char *p = format; *p; p++) {
if (*p == '%' && *(p + 1)) {
p++;
if (*p == '%')
continue; // %% is a literal %, skip
if (*p == 'Z' || *p == 'z') {
needs_subst = true;
break;
}
}
}
if (needs_subst) {
const auto &tz = time::get_global_tz();
char designation[6]; // "+HHMM" + null
int32_t offset = c_tm.tm_isdst > 0 ? tz.dst_offset_seconds : tz.std_offset_seconds;
time::format_designation(offset, designation, sizeof(designation));
char modified[STRFTIME_BUFFER_SIZE];
char *out = modified;
char *out_end = modified + sizeof(modified) - 1;
for (const char *p = format; *p && out < out_end; p++) {
if (*p == '%') {
if (*(p + 1) == '%') {
// %% → copy both percent signs (literal %)
*out++ = *p++;
if (out < out_end)
*out++ = *p;
} else if (*(p + 1) == 'Z' || *(p + 1) == 'z') {
p++; // skip the Z/z
for (const char *d = designation; *d && out < out_end; d++)
*out++ = *d;
} else {
*out++ = *p;
}
} else {
*out++ = *p;
}
}
*out = '\0';
return ::strftime(buffer, buffer_len, modified, &c_tm);
}
#endif
return ::strftime(buffer, buffer_len, format, &c_tm);
}
size_t ESPTime::strftime_to(std::span<char, STRFTIME_BUFFER_SIZE> buffer, const char *format) {
struct tm c_tm = this->to_c_tm();
size_t len = ::strftime(buffer.data(), buffer.size(), format, &c_tm);
size_t len = this->strftime(buffer.data(), buffer.size(), format);
if (len > 0) {
return len;
}