ESPHome 2026.10.0-dev
Loading...
Searching...
No Matches
time.cpp
Go to the documentation of this file.
1#include "time.h" // NOLINT
2#include "helpers.h"
3
4#include <algorithm>
5#ifdef USE_TIME_TIMEZONE
7#endif
8
9namespace esphome {
10
11uint8_t days_in_month(uint8_t month, uint16_t year) {
12 static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
13 if (month == 2 && (year % 4 == 0) && (year % 100 != 0 || year % 400 == 0))
14 return 29;
15 return DAYS_IN_MONTH[month];
16}
17
18size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) {
19 struct tm c_tm = this->to_c_tm();
20#ifdef USE_TIME_TIMEZONE
21 // ::strftime uses libc's internal timezone state for %Z and %z, but we
22 // eliminated setenv("TZ")/tzset() on embedded platforms to save flash.
23 // Substitute %Z and %z with correct values from our parsed timezone.
24 // Quick scan: does format contain %Z or %z (but not %%Z/%%z)?
25 bool needs_subst = false;
26 for (const char *p = format; *p; p++) {
27 if (*p == '%' && *(p + 1)) {
28 p++;
29 if (*p == '%')
30 continue; // %% is a literal %, skip
31 if (*p == 'Z' || *p == 'z') {
32 needs_subst = true;
33 break;
34 }
35 }
36 }
37 if (needs_subst) {
38 const auto &tz = time::get_global_tz();
39 char designation[6]; // "+HHMM" + null
40 int32_t offset = c_tm.tm_isdst > 0 ? tz.dst_offset_seconds : tz.std_offset_seconds;
41 time::format_designation(offset, designation, sizeof(designation));
42
43 char modified[STRFTIME_BUFFER_SIZE];
44 char *out = modified;
45 char *out_end = modified + sizeof(modified) - 1;
46 for (const char *p = format; *p && out < out_end; p++) {
47 if (*p == '%') {
48 if (*(p + 1) == '%') {
49 // %% → copy both percent signs (literal %)
50 *out++ = *p++;
51 if (out < out_end)
52 *out++ = *p;
53 } else if (*(p + 1) == 'Z' || *(p + 1) == 'z') {
54 p++; // skip the Z/z
55 for (const char *d = designation; *d && out < out_end; d++)
56 *out++ = *d;
57 } else {
58 *out++ = *p;
59 }
60 } else {
61 *out++ = *p;
62 }
63 }
64 *out = '\0';
65 return ::strftime(buffer, buffer_len, modified, &c_tm);
66 }
67#endif
68 return ::strftime(buffer, buffer_len, format, &c_tm);
69}
70
71size_t ESPTime::strftime_to(std::span<char, STRFTIME_BUFFER_SIZE> buffer, const char *format) {
72 size_t len = this->strftime(buffer.data(), buffer.size(), format);
73 if (len > 0) {
74 return len;
75 }
76 // Write "ERROR" to buffer on failure for consistent behavior
77 constexpr char error_str[] = "ERROR";
78 std::copy_n(error_str, sizeof(error_str), buffer.data());
79 return sizeof(error_str) - 1; // Length excluding null terminator
80}
81
82ESPTime ESPTime::from_c_tm(struct tm *c_tm, time_t c_time) {
83 ESPTime res{};
84 res.second = uint8_t(c_tm->tm_sec);
85 res.minute = uint8_t(c_tm->tm_min);
86 res.hour = uint8_t(c_tm->tm_hour);
87 res.day_of_week = uint8_t(c_tm->tm_wday + 1);
88 res.day_of_month = uint8_t(c_tm->tm_mday);
89 res.day_of_year = uint16_t(c_tm->tm_yday + 1);
90 res.month = uint8_t(c_tm->tm_mon + 1);
91 res.year = uint16_t(c_tm->tm_year + 1900);
92 res.is_dst = bool(c_tm->tm_isdst);
93 res.timestamp = c_time;
94 return res;
95}
96
97struct tm ESPTime::to_c_tm() {
98 struct tm c_tm {};
99 c_tm.tm_sec = this->second;
100 c_tm.tm_min = this->minute;
101 c_tm.tm_hour = this->hour;
102 c_tm.tm_mday = this->day_of_month;
103 c_tm.tm_mon = this->month - 1;
104 c_tm.tm_year = this->year - 1900;
105 c_tm.tm_wday = this->day_of_week - 1;
106 c_tm.tm_yday = this->day_of_year - 1;
107 c_tm.tm_isdst = this->is_dst;
108 return c_tm;
109}
110
111std::string ESPTime::strftime(const char *format) {
112 char buf[STRFTIME_BUFFER_SIZE];
113 size_t len = this->strftime_to(buf, format);
114 return std::string(buf, len);
115}
116
117// Helper to parse exactly N digits, returns false if not enough digits
118static bool parse_digits(const char *&p, const char *end, int count, uint16_t &value) {
119 value = 0;
120 for (int i = 0; i < count; i++) {
121 if (p >= end || *p < '0' || *p > '9')
122 return false;
123 value = value * 10 + (*p - '0');
124 p++;
125 }
126 return true;
127}
128
129// Helper to check for expected character
130static bool expect_char(const char *&p, const char *end, char expected) {
131 if (p >= end || *p != expected)
132 return false;
133 p++;
134 return true;
135}
136
137bool ESPTime::strptime(const char *time_to_parse, size_t len, ESPTime &esp_time) {
138 // Supported formats:
139 // YYYY-MM-DD HH:MM:SS (19 chars)
140 // YYYY-MM-DD HH:MM (16 chars)
141 // YYYY-MM-DD (10 chars)
142 // HH:MM:SS (8 chars)
143 // HH:MM (5 chars)
144
145 if (time_to_parse == nullptr || len == 0)
146 return false;
147
148 const char *p = time_to_parse;
149 const char *end = time_to_parse + len;
150 uint16_t v1, v2, v3, v4, v5, v6;
151
152 // Try date formats first (start with 4-digit year)
153 if (len >= 10 && time_to_parse[4] == '-') {
154 // YYYY-MM-DD...
155 if (!parse_digits(p, end, 4, v1))
156 return false;
157 if (!expect_char(p, end, '-'))
158 return false;
159 if (!parse_digits(p, end, 2, v2))
160 return false;
161 if (!expect_char(p, end, '-'))
162 return false;
163 if (!parse_digits(p, end, 2, v3))
164 return false;
165
166 esp_time.year = v1;
167 esp_time.month = v2;
168 esp_time.day_of_month = v3;
169
170 if (p == end) {
171 // YYYY-MM-DD (date only)
172 return true;
173 }
174
175 if (!expect_char(p, end, ' '))
176 return false;
177
178 // Continue with time part: HH:MM[:SS]
179 if (!parse_digits(p, end, 2, v4))
180 return false;
181 if (!expect_char(p, end, ':'))
182 return false;
183 if (!parse_digits(p, end, 2, v5))
184 return false;
185
186 esp_time.hour = v4;
187 esp_time.minute = v5;
188
189 if (p == end) {
190 // YYYY-MM-DD HH:MM
191 esp_time.second = 0;
192 return true;
193 }
194
195 if (!expect_char(p, end, ':'))
196 return false;
197 if (!parse_digits(p, end, 2, v6))
198 return false;
199
200 esp_time.second = v6;
201 return p == end; // YYYY-MM-DD HH:MM:SS
202 }
203
204 // Try time-only formats (HH:MM[:SS])
205 if (len >= 5 && time_to_parse[2] == ':') {
206 if (!parse_digits(p, end, 2, v1))
207 return false;
208 if (!expect_char(p, end, ':'))
209 return false;
210 if (!parse_digits(p, end, 2, v2))
211 return false;
212
213 esp_time.hour = v1;
214 esp_time.minute = v2;
215
216 if (p == end) {
217 // HH:MM
218 esp_time.second = 0;
219 return true;
220 }
221
222 if (!expect_char(p, end, ':'))
223 return false;
224 if (!parse_digits(p, end, 2, v3))
225 return false;
226
227 esp_time.second = v3;
228 return p == end; // HH:MM:SS
229 }
230
231 return false;
232}
233
235 this->timestamp++;
236 if (!increment_time_value(this->second, 0, 60))
237 return;
238
239 // second roll-over, increment minute
240 if (!increment_time_value(this->minute, 0, 60))
241 return;
242
243 // minute roll-over, increment hour
244 if (!increment_time_value(this->hour, 0, 24))
245 return;
246
247 // hour roll-over, increment day
249
250 if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) {
251 // day of month roll-over, increment month
252 increment_time_value(this->month, 1, 13);
253 }
254
255 uint16_t days_in_year = (this->year % 4 == 0) ? 366 : 365;
256 if (increment_time_value(this->day_of_year, 1, days_in_year + 1)) {
257 // day of year roll-over, increment year
258 this->year++;
259 }
260}
261
263 this->timestamp += 86400;
264
265 // increment day
267
268 if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) {
269 // day of month roll-over, increment month
270 increment_time_value(this->month, 1, 13);
271 }
272
273 uint16_t days_in_year = (this->year % 4 == 0) ? 366 : 365;
274 if (increment_time_value(this->day_of_year, 1, days_in_year + 1)) {
275 // day of year roll-over, increment year
276 this->year++;
277 }
278}
279
280void ESPTime::recalc_timestamp_utc(bool use_day_of_year) {
281 time_t res = 0;
282 if (!this->fields_in_range(false, use_day_of_year)) {
283 this->timestamp = -1;
284 return;
285 }
286
287 for (int i = 1970; i < this->year; i++)
288 res += (i % 4 == 0) ? 366 : 365;
289
290 if (use_day_of_year) {
291 res += this->day_of_year - 1;
292 } else {
293 for (int i = 1; i < this->month; i++)
294 res += days_in_month(i, this->year);
295 res += this->day_of_month - 1;
296 }
297
298 res *= 24;
299 res += this->hour;
300 res *= 60;
301 res += this->minute;
302 res *= 60;
303 res += this->second;
304 this->timestamp = res;
305}
306
308#ifdef USE_TIME_TIMEZONE
309 // Calculate timestamp as if fields were UTC
310 this->recalc_timestamp_utc(false);
311 if (this->timestamp == -1) {
312 return; // Invalid time
313 }
314
315 // Now convert from local to UTC by adding the offset
316 // POSIX: local = utc - offset, so utc = local + offset
317 const auto &tz = time::get_global_tz();
318
319 if (!tz.has_dst()) {
320 // No DST - just apply standard offset
321 this->timestamp += tz.std_offset_seconds;
322 return;
323 }
324
325 // Try both interpretations to match libc mktime() with tm_isdst=-1
326 // For ambiguous times (fall-back repeated hour), prefer standard time
327 // For invalid times (spring-forward skipped hour), libc normalizes forward
328 time_t utc_if_dst = this->timestamp + tz.dst_offset_seconds;
329 time_t utc_if_std = this->timestamp + tz.std_offset_seconds;
330
331 bool dst_valid = time::is_in_dst(utc_if_dst, tz);
332 bool std_valid = !time::is_in_dst(utc_if_std, tz);
333
334 if (dst_valid && !std_valid) {
335 // Only DST interpretation is valid
336 this->timestamp = utc_if_dst;
337 } else {
338 // All other cases use standard offset:
339 // - Both valid (ambiguous fall-back repeated hour): prefer standard time
340 // - Only standard valid: straightforward
341 // - Neither valid (spring-forward skipped hour): std offset normalizes
342 // forward to match libc mktime(), e.g. 02:30 CST -> 03:30 CDT
343 this->timestamp = utc_if_std;
344 }
345#else
346 // No timezone support - treat as UTC
347 this->recalc_timestamp_utc(false);
348#endif
349}
350
352#ifdef USE_TIME_TIMEZONE
353 time_t now = ::time(nullptr);
354 const auto &tz = time::get_global_tz();
355 // POSIX offset is positive west, but we return offset to add to UTC to get local
356 // So we negate the POSIX offset
357 if (time::is_in_dst(now, tz)) {
358 return -tz.dst_offset_seconds;
359 }
360 return -tz.std_offset_seconds;
361#else
362 // No timezone support - no offset
363 return 0;
364#endif
365}
366
367bool ESPTime::operator<(const ESPTime &other) const { return this->timestamp < other.timestamp; }
368bool ESPTime::operator<=(const ESPTime &other) const { return this->timestamp <= other.timestamp; }
369bool ESPTime::operator==(const ESPTime &other) const { return this->timestamp == other.timestamp; }
370bool ESPTime::operator>=(const ESPTime &other) const { return this->timestamp >= other.timestamp; }
371bool ESPTime::operator>(const ESPTime &other) const { return this->timestamp > other.timestamp; }
372
373template<typename T> bool increment_time_value(T &current, uint16_t begin, uint16_t end) {
374 current++;
375 if (current >= end) {
376 current = begin;
377 return true;
378 }
379 return false;
380}
381
382} // namespace esphome
uint8_t month
Definition date_entity.h:1
uint16_t year
Definition date_entity.h:0
uint8_t second
uint8_t minute
uint8_t hour
const char * format
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
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
uint8_t days_in_month(uint8_t month, uint16_t year)
Definition time.cpp:11
const void size_t len
Definition hal.h:64
bool increment_time_value(T &current, uint16_t begin, uint16_t end)
Definition time.cpp:373
A more user-friendly version of struct tm from time.h.
Definition time.h:23
void increment_second()
Increment this clock instance by one second.
Definition time.cpp:234
static int32_t timezone_offset()
Definition time.cpp:351
uint8_t minute
minutes after the hour [0-59]
Definition time.h:32
void recalc_timestamp_utc(bool use_day_of_year=true)
Recalculate the timestamp field from the other fields of this ESPTime instance (must be UTC).
Definition time.cpp:280
bool operator<(const ESPTime &other) const
Definition time.cpp:367
uint8_t second
seconds after the minute [0-60]
Definition time.h:30
size_t strftime(char *buffer, size_t buffer_len, const char *format)
Convert this ESPTime struct to a null-terminated c string buffer as specified by the format argument.
Definition time.cpp:18
bool fields_in_range(bool check_day_of_week=true, bool check_day_of_year=true) const
Check if time fields are in range.
Definition time.h:89
size_t strftime_to(std::span< char, STRFTIME_BUFFER_SIZE > buffer, const char *format)
Format time into a fixed-size buffer, returns length written.
Definition time.cpp:71
bool operator>(const ESPTime &other) const
Definition time.cpp:371
uint8_t hour
hours since midnight [0-23]
Definition time.h:34
time_t timestamp
unix epoch time (seconds since UTC Midnight January 1, 1970)
Definition time.h:48
void increment_day()
Increment this clock instance by one day.
Definition time.cpp:262
uint16_t day_of_year
day of the year [1-366]
Definition time.h:40
static ESPTime from_c_tm(struct tm *c_tm, time_t c_time)
Convert a C tm struct instance with a C unix epoch timestamp to an ESPTime instance.
Definition time.cpp:82
static bool strptime(const char *time_to_parse, size_t len, ESPTime &esp_time)
Convert a string to ESPTime struct as specified by the format argument.
Definition time.cpp:137
bool operator<=(const ESPTime &other) const
Definition time.cpp:368
bool operator==(const ESPTime &other) const
Definition time.cpp:369
void recalc_timestamp_local()
Recalculate the timestamp field from the other fields of this ESPTime instance assuming local fields.
Definition time.cpp:307
static constexpr size_t STRFTIME_BUFFER_SIZE
Buffer size required for strftime output.
Definition time.h:25
uint8_t day_of_month
day of the month [1-31]
Definition time.h:38
struct tm to_c_tm()
Convert this ESPTime instance back to a tm struct.
Definition time.cpp:97
uint16_t year
year
Definition time.h:44
uint8_t month
month; january=1 [1-12]
Definition time.h:42
bool operator>=(const ESPTime &other) const
Definition time.cpp:370
uint8_t day_of_week
day of the week; sunday=1 [1-7]
Definition time.h:36
bool has_dst() const
Check if this timezone has DST rules.
Definition posix_tz.h:36
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
uint8_t end[39]
Definition sun_gtil2.cpp:17