[time] Fix strftime %Z and %z returning GMT/+0000 instead of actual timezone

Since the timezone bloat elimination (#13635) removed setenv("TZ")/tzset()
on embedded platforms, libc's ::strftime no longer has access to timezone
state for %Z and %z format specifiers. The time values (hour, minute, etc.)
are correct since they come from our custom timezone parser via struct tm
fields, but %Z and %z read from libc's internal _tzname/_timezone which
are stuck at GMT/+0000.

Fix by substituting %Z and %z in the format string with the correct
"+HHMM"/"-HHMM" designation computed from our parsed timezone offset
before passing to ::strftime. The substitution only runs when %Z or %z
are present in the format string, so there is no overhead for the common
case.

Also refactors strftime_to() to delegate to strftime() instead of
duplicating the ::strftime call, ensuring both code paths get the fix.
This commit is contained in:
J. Nick Koston
2026-03-30 21:26:23 -10:00
parent a3913b98ba
commit 38e3095e35
3 changed files with 54 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).
+38 -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,45 @@ 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?
bool needs_subst = false;
for (const char *p = format; *p; p++) {
if (*p == '%' && (*(p + 1) == 'Z' || *(p + 1) == '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 == '%' && (*(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;
}
}
*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;
}