ESPHome 2026.10.0-dev
Loading...
Searching...
No Matches
posix_tz.cpp
Go to the documentation of this file.
2
3#ifdef USE_TIME_TIMEZONE
4
5#include "posix_tz.h"
6#include <cstdio>
7
8namespace esphome::time {
9
10// Global timezone - set once at startup, rarely changes
11// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) - intentional mutable state
12static ParsedTimezone global_tz_{};
13
14void set_global_tz(const ParsedTimezone &tz) { global_tz_ = tz; }
15
16const ParsedTimezone &get_global_tz() { return global_tz_; }
17
18namespace internal {
19
20bool is_leap_year(int year) { return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); }
21
22// Get days in year (avoids duplicate is_leap_year calls)
23static inline int days_in_year(int year) { return is_leap_year(year) ? 366 : 365; }
24
25// Count leap years in [1, year] (i.e. up to and including year)
26static constexpr int count_leap_years_up_to(int year) { return year / 4 - year / 100 + year / 400; }
27
28constexpr int EPOCH_YEAR = 1970;
29constexpr int LEAP_YEARS_BEFORE_EPOCH = count_leap_years_up_to(EPOCH_YEAR - 1);
30constexpr int DAYS_PER_YEAR = 365;
31constexpr int SECONDS_PER_DAY = 86400;
32
33// Days from epoch (Jan 1 1970) to Jan 1 of given year — O(1)
34static inline int64_t days_to_year_start(int year) {
35 return static_cast<int64_t>(DAYS_PER_YEAR) * (year - EPOCH_YEAR) +
36 (count_leap_years_up_to(year - 1) - LEAP_YEARS_BEFORE_EPOCH);
37}
38
39// Convert days since epoch to year, updating days to day-of-year remainder.
40// The initial estimate from days/365 can overshoot by multiple years for
41// far-future dates (e.g., year 5000+) due to accumulated leap days,
42// so we use loops rather than single-step correction.
43static int days_to_year(int64_t &days) {
44 int year = static_cast<int>(EPOCH_YEAR + days / DAYS_PER_YEAR);
45 int64_t year_start = days_to_year_start(year);
46 while (days < year_start) {
47 year--;
48 year_start = days_to_year_start(year);
49 }
50 while (days >= year_start + days_in_year(year)) {
51 year_start += days_in_year(year);
52 year++;
53 }
54 days -= year_start;
55 return year;
56}
57
58// Extract just the year from a UTC epoch — O(1)
59static int epoch_to_year(time_t epoch) {
60 int64_t days = epoch / SECONDS_PER_DAY;
61 if (epoch < 0 && epoch % SECONDS_PER_DAY != 0)
62 days--;
63 return days_to_year(days);
64}
65
66int days_in_month(int year, int month) {
67 switch (month) {
68 case 2:
69 return is_leap_year(year) ? 29 : 28;
70 case 4:
71 case 6:
72 case 9:
73 case 11:
74 return 30;
75 default:
76 return 31;
77 }
78}
79
80// Zeller-like algorithm for day of week (0 = Sunday)
81int __attribute__((noinline)) day_of_week(int year, int month, int day) {
82 // Adjust for January/February
83 if (month < 3) {
84 month += 12;
85 year--;
86 }
87 int k = year % 100;
88 int j = year / 100;
89 int h = (day + (13 * (month + 1)) / 5 + k + k / 4 + j / 4 - 2 * j) % 7;
90 // Convert from Zeller (0=Sat) to standard (0=Sun)
91 return ((h + 6) % 7);
92}
93
94void __attribute__((noinline)) epoch_to_tm_utc(time_t epoch, struct tm *out_tm) {
95 // Days since epoch
96 int64_t days = epoch / SECONDS_PER_DAY;
98 if (remaining_secs < 0) {
99 days--;
101 }
102
103 out_tm->tm_sec = remaining_secs % 60;
104 remaining_secs /= 60;
105 out_tm->tm_min = remaining_secs % 60;
106 out_tm->tm_hour = remaining_secs / 60;
107
108 // Day of week (Jan 1, 1970 was Thursday = 4)
109 out_tm->tm_wday = static_cast<int>((days + 4) % 7);
110 if (out_tm->tm_wday < 0)
111 out_tm->tm_wday += 7;
112
113 // Calculate year (updates days to day-of-year)
114 int year = days_to_year(days);
115 out_tm->tm_year = year - 1900;
116 out_tm->tm_yday = static_cast<int>(days);
117
118 // Calculate month and day
119 int month = 1;
120 int dim;
121 while (days >= (dim = days_in_month(year, month))) {
122 days -= dim;
123 month++;
124 }
125
126 out_tm->tm_mon = month - 1;
127 out_tm->tm_mday = static_cast<int>(days) + 1;
128 out_tm->tm_isdst = 0;
129}
130
131void __attribute__((noinline)) julian_to_month_day(int julian_day, int &out_month, int &out_day) {
132 // J format: day 1-365, Feb 29 is NOT counted even in leap years
133 // So day 60 is always March 1
134 // Iterate forward through months (no array needed)
135 int remaining = julian_day;
136 out_month = 1;
137 while (out_month <= 12) {
138 // Days in month for non-leap year (J format ignores leap years)
139 int dim = days_in_month(2001, out_month); // 2001 is non-leap year
140 if (remaining <= dim) {
141 out_day = remaining;
142 return;
143 }
144 remaining -= dim;
145 out_month++;
146 }
147 out_day = remaining;
148}
149
150void __attribute__((noinline)) day_of_year_to_month_day(int day_of_year, int year, int &out_month, int &out_day) {
151 // Plain format: day 0-365, Feb 29 IS counted in leap years
152 // Day 0 = Jan 1
153 int remaining = day_of_year;
154 out_month = 1;
155
156 while (out_month <= 12) {
157 int days_this_month = days_in_month(year, out_month);
158 if (remaining < days_this_month) {
159 out_day = remaining + 1;
160 return;
161 }
162 remaining -= days_this_month;
163 out_month++;
164 }
165
166 // Shouldn't reach here with valid input
167 out_month = 12;
168 out_day = 31;
169}
170
171// Calculate days from Jan 1 of given year to given month/day
172static int __attribute__((noinline)) days_from_year_start(int year, int month, int day) {
173 int days = day - 1;
174 for (int m = 1; m < month; m++) {
176 }
177 return days;
178}
179
181 int month = 1;
182 int day = 1;
183
184 switch (rule.type) {
186 // Find the nth occurrence of day_of_week in the given month
187 int first_dow = day_of_week(year, rule.month, 1);
188
189 // Days until first occurrence of target day
190 int days_until_first = (rule.day_of_week - first_dow + 7) % 7;
191 int first_occurrence = 1 + days_until_first;
192
193 if (rule.week == 5) {
194 // "Last" occurrence - find the last one in the month
196 day = first_occurrence;
197 while (day + 7 <= dim) {
198 day += 7;
199 }
200 } else {
201 // nth occurrence
202 day = first_occurrence + (rule.week - 1) * 7;
203 }
204 month = rule.month;
205 break;
206 }
207
209 // J format: day 1-365, Feb 29 not counted
211 break;
212
214 // Plain format: day 0-365, Feb 29 counted
216 break;
217
219 // Should never be called with NONE, but handle it gracefully
220 month = 1;
221 day = 1;
222 break;
223 }
224
225 // Calculate days from epoch to this date
226 int64_t days = days_to_year_start(year) + days_from_year_start(year, month, day);
227
228 // Convert to epoch and add transition time and base offset
230}
231
232} // namespace internal
233
234bool __attribute__((noinline)) is_in_dst(time_t utc_epoch, const ParsedTimezone &tz) {
235 if (!tz.has_dst()) {
236 return false;
237 }
238
239 int year = internal::epoch_to_year(utc_epoch);
240
241 // Calculate DST start and end for this year
242 // DST start transition happens in standard time
244 // DST end transition happens in daylight time
246
248 // Northern hemisphere: DST is between start and end
249 return (utc_epoch >= dst_start && utc_epoch < dst_end);
250 } else {
251 // Southern hemisphere: DST is outside the range (wraps around year)
252 return (utc_epoch >= dst_start || utc_epoch < dst_end);
253 }
254}
255
256// Format a POSIX offset (positive = west) as "+HHMM" / "-HHMM" for display.
257// Convention: negate POSIX sign so east-of-UTC is positive (ISO 8601 / RFC 2822).
258void format_designation(int32_t posix_offset, char *buf, size_t buf_size) {
259 int32_t display = -posix_offset;
260 char sign = display >= 0 ? '+' : '-';
261 if (display < 0)
262 display = -display;
263 int h = display / 3600;
264 int m = (display % 3600) / 60;
265 snprintf(buf, buf_size, "%c%02d%02d", sign, h, m);
266}
267
268bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm) {
269 if (!out_tm) {
270 return false;
271 }
272
273 // Determine DST status once (avoids duplicate is_in_dst calculation)
274 bool in_dst = is_in_dst(utc_epoch, tz);
275 int32_t offset = in_dst ? tz.dst_offset_seconds : tz.std_offset_seconds;
276
277 // Apply offset (POSIX offset is positive west, so subtract to get local)
278 time_t local_epoch = utc_epoch - offset;
279
280 internal::epoch_to_tm_utc(local_epoch, out_tm);
281 out_tm->tm_isdst = in_dst ? 1 : 0;
282
283 return true;
284}
285
286} // namespace esphome::time
287
288#ifndef USE_HOST
289// Override libc's localtime functions to use our timezone on embedded platforms.
290// This allows user lambdas calling ::localtime() to get correct local time
291// without needing the TZ environment variable (which pulls in scanf bloat).
292// On host, we use the normal TZ mechanism since there's no memory constraint.
293
294// Thread-safe version
295extern "C" struct tm *localtime_r(const time_t *timer, struct tm *result) {
296 if (timer == nullptr || result == nullptr) {
297 return nullptr;
298 }
300 return result;
301}
302
303// Non-thread-safe version (uses static buffer, standard libc behavior)
304extern "C" struct tm *localtime(const time_t *timer) {
305 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
306 static struct tm localtime_buf;
307 return localtime_r(timer, &localtime_buf);
308}
309#endif // !USE_HOST
310
311#endif // USE_TIME_TIMEZONE
uint8_t m
Definition bl0906.h:1
uint8_t h
Definition bl0906.h:2
uint16_t year
Definition date_entity.h:0
void int int & out_day
Definition posix_tz.cpp:131
int day_of_week(int year, int month, int day)
Calculate day of week for any date (0 = Sunday) Uses a simplified algorithm that works for years 1970...
int days_in_month(int year, int month)
Get the number of days in a month.
Definition posix_tz.cpp:66
constexpr int LEAP_YEARS_BEFORE_EPOCH
Definition posix_tz.cpp:29
void struct tm * out_tm
Definition posix_tz.cpp:94
time_t const DSTRule & rule
Definition posix_tz.cpp:180
constexpr int SECONDS_PER_DAY
Definition posix_tz.cpp:31
void julian_to_month_day(int julian_day, int &month, int &day)
Convert Julian day (J format, 1-365 not counting Feb 29) to month/day.
void day_of_year_to_month_day(int day_of_year, int year, int &month, int &day)
Convert day of year (plain format, 0-365 counting Feb 29) to month/day.
constexpr int EPOCH_YEAR
Definition posix_tz.cpp:28
bool is_leap_year(int year)
Check if a year is a leap year.
Definition posix_tz.cpp:20
time_t const DSTRule int32_t base_offset_seconds
Definition posix_tz.cpp:180
constexpr int DAYS_PER_YEAR
Definition posix_tz.cpp:30
time_t calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds)
Calculate the epoch timestamp for a DST transition in a given year.
void epoch_to_tm_utc(time_t epoch, struct tm *out_tm)
Convert epoch to year/month/day/hour/min/sec (UTC)
void set_global_tz(const ParsedTimezone &tz)
Set the global timezone used by epoch_to_local_tm() when called without a timezone.
Definition posix_tz.cpp:14
bool is_in_dst(time_t utc_epoch, const ParsedTimezone &tz)
Check if a given UTC epoch falls within DST for the parsed timezone.
const ParsedTimezone & get_global_tz()
Get the global timezone.
Definition posix_tz.cpp:16
ESPTime __attribute__((noinline)) RealTimeClock
@ JULIAN_NO_LEAP
J format: Jn (day 1-365, Feb 29 not counted)
@ NONE
No DST rule (used to indicate no DST)
@ DAY_OF_YEAR
Plain number: n (day 0-365, Feb 29 counted in leap years)
@ MONTH_WEEK_DAY
M format: Mm.w.d (e.g., M3.2.0 = 2nd Sunday of March)
bool const ParsedTimezone & tz
Definition posix_tz.cpp:234
time_t dst_start
Definition posix_tz.cpp:243
bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm)
Convert a UTC epoch to local time using the parsed timezone.
Definition posix_tz.cpp:268
void format_designation(int32_t posix_offset, char *buf, size_t buf_size)
Format a POSIX offset as "+HHMM"/"-HHMM" into buf (must be >= 6 bytes).
Definition posix_tz.cpp:258
struct tm * localtime_r(const time_t *timer, struct tm *result)
Definition posix_tz.cpp:295
struct tm * localtime(const time_t *timer)
Definition posix_tz.cpp:304
Rule for DST transition (packed for 32-bit: 12 bytes)
Definition posix_tz.h:19
uint16_t day
Day of year (for JULIAN_NO_LEAP and DAY_OF_YEAR)
Definition posix_tz.h:21
DSTRuleType type
Type of rule.
Definition posix_tz.h:22
uint8_t week
Week 1-5, 5 = last (for MONTH_WEEK_DAY)
Definition posix_tz.h:24
int32_t time_seconds
Seconds after midnight (default 7200 = 2:00 AM)
Definition posix_tz.h:20
uint8_t day_of_week
Day 0-6, 0 = Sunday (for MONTH_WEEK_DAY)
Definition posix_tz.h:25
uint8_t month
Month 1-12 (for MONTH_WEEK_DAY)
Definition posix_tz.h:23
Parsed POSIX timezone information (packed for 32-bit: 32 bytes)
Definition posix_tz.h:29
bool has_dst() const
Check if this timezone has DST rules.
Definition posix_tz.h:36
DSTRule dst_end
When DST ends.
Definition posix_tz.h:33
DSTRule dst_start
When DST starts.
Definition posix_tz.h:32
int32_t dst_offset_seconds
DST offset from UTC in seconds.
Definition posix_tz.h:31
int32_t std_offset_seconds
Standard time offset from UTC in seconds (positive = west)
Definition posix_tz.h:30