mirror of
https://github.com/esphome/esphome.git
synced 2026-02-25 04:45:29 -07:00
Merge branch 'remove_posix_tz_parser' into integration
This commit is contained in:
@@ -1113,30 +1113,23 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
|
||||
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
if (!value.timezone.empty()) {
|
||||
// Check if the sender provided pre-parsed timezone data.
|
||||
// If std_offset is non-zero or DST rules are present, the parsed data was populated.
|
||||
// For UTC (all zeros), string parsing produces the same result, so the fallback is equivalent.
|
||||
const auto &pt = value.parsed_timezone;
|
||||
if (pt.std_offset_seconds != 0 || pt.dst_start.type != enums::DST_RULE_TYPE_NONE) {
|
||||
time::ParsedTimezone tz{};
|
||||
tz.std_offset_seconds = pt.std_offset_seconds;
|
||||
tz.dst_offset_seconds = pt.dst_offset_seconds;
|
||||
tz.dst_start.time_seconds = pt.dst_start.time_seconds;
|
||||
tz.dst_start.day = static_cast<uint16_t>(pt.dst_start.day);
|
||||
tz.dst_start.type = static_cast<time::DSTRuleType>(pt.dst_start.type);
|
||||
tz.dst_start.month = static_cast<uint8_t>(pt.dst_start.month);
|
||||
tz.dst_start.week = static_cast<uint8_t>(pt.dst_start.week);
|
||||
tz.dst_start.day_of_week = static_cast<uint8_t>(pt.dst_start.day_of_week);
|
||||
tz.dst_end.time_seconds = pt.dst_end.time_seconds;
|
||||
tz.dst_end.day = static_cast<uint16_t>(pt.dst_end.day);
|
||||
tz.dst_end.type = static_cast<time::DSTRuleType>(pt.dst_end.type);
|
||||
tz.dst_end.month = static_cast<uint8_t>(pt.dst_end.month);
|
||||
tz.dst_end.week = static_cast<uint8_t>(pt.dst_end.week);
|
||||
tz.dst_end.day_of_week = static_cast<uint8_t>(pt.dst_end.day_of_week);
|
||||
time::set_global_tz(tz);
|
||||
} else {
|
||||
homeassistant::global_homeassistant_time->set_timezone(value.timezone.c_str(), value.timezone.size());
|
||||
}
|
||||
time::ParsedTimezone tz{};
|
||||
tz.std_offset_seconds = pt.std_offset_seconds;
|
||||
tz.dst_offset_seconds = pt.dst_offset_seconds;
|
||||
tz.dst_start.time_seconds = pt.dst_start.time_seconds;
|
||||
tz.dst_start.day = static_cast<uint16_t>(pt.dst_start.day);
|
||||
tz.dst_start.type = static_cast<time::DSTRuleType>(pt.dst_start.type);
|
||||
tz.dst_start.month = static_cast<uint8_t>(pt.dst_start.month);
|
||||
tz.dst_start.week = static_cast<uint8_t>(pt.dst_start.week);
|
||||
tz.dst_start.day_of_week = static_cast<uint8_t>(pt.dst_start.day_of_week);
|
||||
tz.dst_end.time_seconds = pt.dst_end.time_seconds;
|
||||
tz.dst_end.day = static_cast<uint16_t>(pt.dst_end.day);
|
||||
tz.dst_end.type = static_cast<time::DSTRuleType>(pt.dst_end.type);
|
||||
tz.dst_end.month = static_cast<uint8_t>(pt.dst_end.month);
|
||||
tz.dst_end.week = static_cast<uint8_t>(pt.dst_end.week);
|
||||
tz.dst_end.day_of_week = static_cast<uint8_t>(pt.dst_end.day_of_week);
|
||||
time::set_global_tz(tz);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -362,11 +362,12 @@ async def setup_time_core_(time_var, config):
|
||||
|
||||
if CORE.is_host:
|
||||
# Host platform needs setenv("TZ")/tzset() for libc compatibility
|
||||
cg.add(time_var.set_timezone(timezone))
|
||||
else:
|
||||
# Embedded: pre-parse at codegen time, emit struct directly
|
||||
parsed = parse_posix_tz_python(timezone)
|
||||
_emit_parsed_timezone_fields(parsed)
|
||||
cg.add(cg.RawExpression(f'setenv("TZ", "{timezone}", 1)'))
|
||||
cg.add(cg.RawExpression("tzset()"))
|
||||
|
||||
# Pre-parse at codegen time, emit struct directly
|
||||
parsed = parse_posix_tz_python(timezone)
|
||||
_emit_parsed_timezone_fields(parsed)
|
||||
|
||||
for conf in config.get(CONF_ON_TIME, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
|
||||
#include "posix_tz.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace esphome::time {
|
||||
|
||||
@@ -17,17 +16,6 @@ const ParsedTimezone &get_global_tz() { return global_tz_; }
|
||||
|
||||
namespace internal {
|
||||
|
||||
// Remove before 2026.9.0: parse_uint, skip_tz_name, parse_offset, parse_dst_rule,
|
||||
// and parse_transition_time are only used by parse_posix_tz() (bridge code).
|
||||
static uint32_t parse_uint(const char *&p) {
|
||||
uint32_t value = 0;
|
||||
while (std::isdigit(static_cast<unsigned char>(*p))) {
|
||||
value = value * 10 + (*p - '0');
|
||||
p++;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
bool is_leap_year(int year) { return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); }
|
||||
|
||||
// Get days in year (avoids duplicate is_leap_year calls)
|
||||
@@ -121,62 +109,6 @@ void __attribute__((noinline)) epoch_to_tm_utc(time_t epoch, struct tm *out_tm)
|
||||
out_tm->tm_isdst = 0;
|
||||
}
|
||||
|
||||
bool skip_tz_name(const char *&p) {
|
||||
if (*p == '<') {
|
||||
// Angle-bracket quoted name: <+07>, <-03>, <AEST>
|
||||
p++; // skip '<'
|
||||
while (*p && *p != '>') {
|
||||
p++;
|
||||
}
|
||||
if (*p == '>') {
|
||||
p++; // skip '>'
|
||||
return true;
|
||||
}
|
||||
return false; // Unterminated
|
||||
}
|
||||
|
||||
// Standard name: 3+ letters
|
||||
const char *start = p;
|
||||
while (*p && std::isalpha(static_cast<unsigned char>(*p))) {
|
||||
p++;
|
||||
}
|
||||
return (p - start) >= 3;
|
||||
}
|
||||
|
||||
int32_t __attribute__((noinline)) parse_offset(const char *&p) {
|
||||
int sign = 1;
|
||||
if (*p == '-') {
|
||||
sign = -1;
|
||||
p++;
|
||||
} else if (*p == '+') {
|
||||
p++;
|
||||
}
|
||||
|
||||
int hours = parse_uint(p);
|
||||
int minutes = 0;
|
||||
int seconds = 0;
|
||||
|
||||
if (*p == ':') {
|
||||
p++;
|
||||
minutes = parse_uint(p);
|
||||
if (*p == ':') {
|
||||
p++;
|
||||
seconds = parse_uint(p);
|
||||
}
|
||||
}
|
||||
|
||||
return sign * (hours * 3600 + minutes * 60 + seconds);
|
||||
}
|
||||
|
||||
// Helper to parse the optional /time suffix (reuses parse_offset logic)
|
||||
static void parse_transition_time(const char *&p, DSTRule &rule) {
|
||||
rule.time_seconds = 2 * 3600; // Default 02:00
|
||||
if (*p == '/') {
|
||||
p++;
|
||||
rule.time_seconds = parse_offset(p);
|
||||
}
|
||||
}
|
||||
|
||||
void __attribute__((noinline)) julian_to_month_day(int julian_day, int &out_month, int &out_day) {
|
||||
// J format: day 1-365, Feb 29 is NOT counted even in leap years
|
||||
// So day 60 is always March 1
|
||||
@@ -217,59 +149,6 @@ void __attribute__((noinline)) day_of_year_to_month_day(int day_of_year, int yea
|
||||
out_day = 31;
|
||||
}
|
||||
|
||||
bool parse_dst_rule(const char *&p, DSTRule &rule) {
|
||||
rule = {}; // Zero initialize
|
||||
|
||||
if (*p == 'M' || *p == 'm') {
|
||||
// M format: Mm.w.d (month.week.day)
|
||||
rule.type = DSTRuleType::MONTH_WEEK_DAY;
|
||||
p++;
|
||||
|
||||
rule.month = parse_uint(p);
|
||||
if (rule.month < 1 || rule.month > 12)
|
||||
return false;
|
||||
|
||||
if (*p++ != '.')
|
||||
return false;
|
||||
|
||||
rule.week = parse_uint(p);
|
||||
if (rule.week < 1 || rule.week > 5)
|
||||
return false;
|
||||
|
||||
if (*p++ != '.')
|
||||
return false;
|
||||
|
||||
rule.day_of_week = parse_uint(p);
|
||||
if (rule.day_of_week > 6)
|
||||
return false;
|
||||
|
||||
} else if (*p == 'J' || *p == 'j') {
|
||||
// J format: Jn (Julian day 1-365, not counting Feb 29)
|
||||
rule.type = DSTRuleType::JULIAN_NO_LEAP;
|
||||
p++;
|
||||
|
||||
rule.day = parse_uint(p);
|
||||
if (rule.day < 1 || rule.day > 365)
|
||||
return false;
|
||||
|
||||
} else if (std::isdigit(static_cast<unsigned char>(*p))) {
|
||||
// Plain number format: n (day 0-365, counting Feb 29)
|
||||
rule.type = DSTRuleType::DAY_OF_YEAR;
|
||||
|
||||
rule.day = parse_uint(p);
|
||||
if (rule.day > 365)
|
||||
return false;
|
||||
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse optional /time suffix
|
||||
parse_transition_time(p, rule);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Calculate days from Jan 1 of given year to given month/day
|
||||
static int __attribute__((noinline)) days_from_year_start(int year, int month, int day) {
|
||||
int days = day - 1;
|
||||
@@ -365,83 +244,6 @@ bool __attribute__((noinline)) is_in_dst(time_t utc_epoch, const ParsedTimezone
|
||||
}
|
||||
}
|
||||
|
||||
// Remove before 2026.9.0: This parser is bridge code for backward compatibility with
|
||||
// older Home Assistant clients that send the timezone as a POSIX TZ string instead of
|
||||
// the pre-parsed ParsedTimezone protobuf struct. Once all clients send the struct
|
||||
// directly, this function and the parsing helpers above (skip_tz_name, parse_offset,
|
||||
// parse_dst_rule, parse_transition_time) can be removed.
|
||||
// See https://github.com/esphome/backlog/issues/91
|
||||
bool parse_posix_tz(const char *tz_string, ParsedTimezone &result) {
|
||||
if (!tz_string || !*tz_string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *p = tz_string;
|
||||
|
||||
// Initialize result (dst_start/dst_end default to type=NONE, so has_dst() returns false)
|
||||
result.std_offset_seconds = 0;
|
||||
result.dst_offset_seconds = 0;
|
||||
result.dst_start = {};
|
||||
result.dst_end = {};
|
||||
|
||||
// Skip standard timezone name
|
||||
if (!internal::skip_tz_name(p)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse standard offset (required)
|
||||
if (!*p || (!std::isdigit(static_cast<unsigned char>(*p)) && *p != '+' && *p != '-')) {
|
||||
return false;
|
||||
}
|
||||
result.std_offset_seconds = internal::parse_offset(p);
|
||||
|
||||
// Check for DST name
|
||||
if (!*p) {
|
||||
return true; // No DST
|
||||
}
|
||||
|
||||
// If next char is comma, there's no DST name but there are rules (invalid)
|
||||
if (*p == ',') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if there's something that looks like a DST name start
|
||||
// (letter or angle bracket). If not, treat as trailing garbage and return success.
|
||||
if (!std::isalpha(static_cast<unsigned char>(*p)) && *p != '<') {
|
||||
return true; // No DST, trailing characters ignored
|
||||
}
|
||||
|
||||
if (!internal::skip_tz_name(p)) {
|
||||
return false; // Invalid DST name (started but malformed)
|
||||
}
|
||||
|
||||
// Optional DST offset (default is std - 1 hour)
|
||||
if (*p && *p != ',' && (std::isdigit(static_cast<unsigned char>(*p)) || *p == '+' || *p == '-')) {
|
||||
result.dst_offset_seconds = internal::parse_offset(p);
|
||||
} else {
|
||||
result.dst_offset_seconds = result.std_offset_seconds - 3600;
|
||||
}
|
||||
|
||||
// Parse DST rules (required when DST name is present)
|
||||
if (*p != ',') {
|
||||
// DST name without rules - treat as no DST since we can't determine transitions
|
||||
return true;
|
||||
}
|
||||
|
||||
p++;
|
||||
if (!internal::parse_dst_rule(p, result.dst_start)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Second rule is required per POSIX
|
||||
if (*p != ',') {
|
||||
return false;
|
||||
}
|
||||
p++;
|
||||
// has_dst() now returns true since dst_start.type was set by parse_dst_rule
|
||||
return internal::parse_dst_rule(p, result.dst_end);
|
||||
}
|
||||
|
||||
bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm) {
|
||||
if (!out_tm) {
|
||||
return false;
|
||||
|
||||
@@ -36,28 +36,6 @@ struct ParsedTimezone {
|
||||
bool has_dst() const { return this->dst_start.type != DSTRuleType::NONE; }
|
||||
};
|
||||
|
||||
/// Parse a POSIX TZ string into a ParsedTimezone struct.
|
||||
///
|
||||
/// @deprecated Remove before 2026.9.0 (bridge code for backward compatibility).
|
||||
/// This parser only exists so that older Home Assistant clients that send the timezone
|
||||
/// as a string (instead of the pre-parsed ParsedTimezone protobuf struct) can still
|
||||
/// set the timezone on the device. Once all clients are updated to send the struct
|
||||
/// directly, this function and all internal parsing helpers will be removed.
|
||||
/// See https://github.com/esphome/backlog/issues/91
|
||||
///
|
||||
/// Supports formats like:
|
||||
/// - "EST5" (simple offset, no DST)
|
||||
/// - "EST5EDT,M3.2.0,M11.1.0" (with DST, M-format rules)
|
||||
/// - "CST6CDT,M3.2.0/2,M11.1.0/2" (with transition times)
|
||||
/// - "<+07>-7" (angle-bracket notation for special names)
|
||||
/// - "IST-5:30" (half-hour offsets)
|
||||
/// - "EST5EDT,J60,J300" (J-format: Julian day without leap day)
|
||||
/// - "EST5EDT,60,300" (plain day number: day of year with leap day)
|
||||
/// @param tz_string The POSIX TZ string to parse
|
||||
/// @param result Output: the parsed timezone data
|
||||
/// @return true if parsing succeeded, false on error
|
||||
bool parse_posix_tz(const char *tz_string, ParsedTimezone &result);
|
||||
|
||||
/// Convert a UTC epoch to local time using the parsed timezone.
|
||||
/// This replaces libc's localtime() to avoid scanf dependency.
|
||||
/// @param utc_epoch Unix timestamp in UTC
|
||||
@@ -81,29 +59,9 @@ const ParsedTimezone &get_global_tz();
|
||||
bool is_in_dst(time_t utc_epoch, const ParsedTimezone &tz);
|
||||
|
||||
// Internal helper functions exposed for testing.
|
||||
// Remove before 2026.9.0: skip_tz_name, parse_offset, parse_dst_rule are only
|
||||
// used by parse_posix_tz() which is bridge code for backward compatibility.
|
||||
// The remaining helpers (epoch_to_tm_utc, day_of_week, days_in_month, etc.)
|
||||
// are used by the conversion functions and will stay.
|
||||
|
||||
namespace internal {
|
||||
|
||||
/// Skip a timezone name (letters or <...> quoted format)
|
||||
/// @param p Pointer to current position, updated on return
|
||||
/// @return true if a valid name was found
|
||||
bool skip_tz_name(const char *&p);
|
||||
|
||||
/// Parse an offset in format [-]hh[:mm[:ss]]
|
||||
/// @param p Pointer to current position, updated on return
|
||||
/// @return Offset in seconds
|
||||
int32_t parse_offset(const char *&p);
|
||||
|
||||
/// Parse a DST rule in format Mm.w.d[/time], Jn[/time], or n[/time]
|
||||
/// @param p Pointer to current position, updated on return
|
||||
/// @param rule Output: the parsed rule
|
||||
/// @return true if parsing succeeded
|
||||
bool parse_dst_rule(const char *&p, DSTRule &rule);
|
||||
|
||||
/// Convert Julian day (J format, 1-365 not counting Feb 29) to month/day
|
||||
/// @param julian_day Day number 1-365
|
||||
/// @param[out] month Output: month 1-12
|
||||
|
||||
@@ -107,31 +107,4 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
||||
this->time_sync_callback_.call();
|
||||
}
|
||||
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
void RealTimeClock::apply_timezone_(const char *tz) {
|
||||
ParsedTimezone parsed{};
|
||||
|
||||
// Handle null or empty input - use UTC
|
||||
if (tz == nullptr || *tz == '\0') {
|
||||
set_global_tz(parsed);
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_HOST
|
||||
// On host platform, also set TZ environment variable for libc compatibility
|
||||
setenv("TZ", tz, 1);
|
||||
tzset();
|
||||
#endif
|
||||
|
||||
// Parse the POSIX TZ string using our custom parser
|
||||
if (!parse_posix_tz(tz, parsed)) {
|
||||
ESP_LOGW(TAG, "Failed to parse timezone: %s", tz);
|
||||
// parsed stays as default (UTC) on failure
|
||||
}
|
||||
|
||||
// Set global timezone for all time conversions
|
||||
set_global_tz(parsed);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::time
|
||||
|
||||
@@ -22,30 +22,6 @@ class RealTimeClock : public PollingComponent {
|
||||
public:
|
||||
explicit RealTimeClock();
|
||||
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
/// Set the time zone from a POSIX TZ string.
|
||||
void set_timezone(const char *tz) { this->apply_timezone_(tz); }
|
||||
|
||||
/// Set the time zone from a character buffer with known length.
|
||||
/// The buffer does not need to be null-terminated.
|
||||
void set_timezone(const char *tz, size_t len) {
|
||||
if (tz == nullptr) {
|
||||
this->apply_timezone_(nullptr);
|
||||
return;
|
||||
}
|
||||
// Stack buffer - TZ strings from tzdata are typically short (< 50 chars)
|
||||
char buf[128];
|
||||
if (len >= sizeof(buf))
|
||||
len = sizeof(buf) - 1;
|
||||
memcpy(buf, tz, len);
|
||||
buf[len] = '\0';
|
||||
this->apply_timezone_(buf);
|
||||
}
|
||||
|
||||
/// Set the time zone from a std::string.
|
||||
void set_timezone(const std::string &tz) { this->apply_timezone_(tz.c_str()); }
|
||||
#endif
|
||||
|
||||
/// Get the time in the currently defined timezone.
|
||||
ESPTime now();
|
||||
|
||||
@@ -65,10 +41,6 @@ class RealTimeClock : public PollingComponent {
|
||||
/// Report a unix epoch as current time.
|
||||
void synchronize_epoch_(uint32_t epoch);
|
||||
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
void apply_timezone_(const char *tz);
|
||||
#endif
|
||||
|
||||
LazyCallbackManager<void()> time_sync_callback_;
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user