ESPHome 2026.5.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
117std::string ESPTime::strftime(const std::string &format) { return this->strftime(format.c_str()); }
118
119// Helper to parse exactly N digits, returns false if not enough digits
120static bool parse_digits(const char *&p, const char *end, int count, uint16_t &value) {
121 value = 0;
122 for (int i = 0; i < count; i++) {
123 if (p >= end || *p < '0' || *p > '9')
124 return false;
125 value = value * 10 + (*p - '0');
126 p++;
127 }
128 return true;
129}
130
131// Helper to check for expected character
132static bool expect_char(const char *&p, const char *end, char expected) {
133 if (p >= end || *p != expected)
134 return false;
135 p++;
136 return true;
137}
138
139bool ESPTime::strptime(const char *time_to_parse, size_t len, ESPTime &esp_time) {
140 // Supported formats:
141 // YYYY-MM-DD HH:MM:SS (19 chars)
142 // YYYY-MM-DD HH:MM (16 chars)
143 // YYYY-MM-DD (10 chars)
144 // HH:MM:SS (8 chars)
145 // HH:MM (5 chars)
146
147 if (time_to_parse == nullptr || len == 0)
148 return false;
149
150 const char *p = time_to_parse;
151 const char *end = time_to_parse + len;
152 uint16_t v1, v2, v3, v4, v5, v6;
153
154 // Try date formats first (start with 4-digit year)
155 if (len >= 10 && time_to_parse[4] == '-') {
156 // YYYY-MM-DD...
157 if (!parse_digits(p, end, 4, v1))
158 return false;
159 if (!expect_char(p, end, '-'))
160 return false;
161 if (!parse_digits(p, end, 2, v2))
162 return false;
163 if (!expect_char(p, end, '-'))
164 return false;
165 if (!parse_digits(p, end, 2, v3))
166 return false;
167
168 esp_time.year = v1;
169 esp_time.month = v2;
170 esp_time.day_of_month = v3;
171
172 if (p == end) {
173 // YYYY-MM-DD (date only)
174 return true;
175 }
176
177 if (!expect_char(p, end, ' '))
178 return false;
179
180 // Continue with time part: HH:MM[:SS]
181 if (!parse_digits(p, end, 2, v4))
182 return false;
183 if (!expect_char(p, end, ':'))
184 return false;
185 if (!parse_digits(p, end, 2, v5))
186 return false;
187
188 esp_time.hour = v4;
189 esp_time.minute = v5;
190
191 if (p == end) {
192 // YYYY-MM-DD HH:MM
193 esp_time.second = 0;
194 return true;
195 }
196
197 if (!expect_char(p, end, ':'))
198 return false;
199 if (!parse_digits(p, end, 2, v6))
200 return false;
201
202 esp_time.second = v6;
203 return p == end; // YYYY-MM-DD HH:MM:SS
204 }
205
206 // Try time-only formats (HH:MM[:SS])
207 if (len >= 5 && time_to_parse[2] == ':') {
208 if (!parse_digits(p, end, 2, v1))
209 return false;
210 if (!expect_char(p, end, ':'))
211 return false;
212 if (!parse_digits(p, end, 2, v2))
213 return false;
214
215 esp_time.hour = v1;
216 esp_time.minute = v2;
217
218 if (p == end) {
219 // HH:MM
220 esp_time.second = 0;
221 return true;
222 }
223
224 if (!expect_char(p, end, ':'))
225 return false;
226 if (!parse_digits(p, end, 2, v3))
227 return false;
228
229 esp_time.second = v3;
230 return p == end; // HH:MM:SS
231 }
232
233 return false;
234}
235
237 this->timestamp++;
238 if (!increment_time_value(this->second, 0, 60))
239 return;
240
241 // second roll-over, increment minute
242 if (!increment_time_value(this->minute, 0, 60))
243 return;
244
245 // minute roll-over, increment hour
246 if (!increment_time_value(this->hour, 0, 24))
247 return;
248
249 // hour roll-over, increment day
251
252 if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) {
253 // day of month roll-over, increment month
254 increment_time_value(this->month, 1, 13);
255 }
256
257 uint16_t days_in_year = (this->year % 4 == 0) ? 366 : 365;
258 if (increment_time_value(this->day_of_year, 1, days_in_year + 1)) {
259 // day of year roll-over, increment year
260 this->year++;
261 }
262}
263
265 this->timestamp += 86400;
266
267 // increment day
269
270 if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) {
271 // day of month roll-over, increment month
272 increment_time_value(this->month, 1, 13);
273 }
274
275 uint16_t days_in_year = (this->year % 4 == 0) ? 366 : 365;
276 if (increment_time_value(this->day_of_year, 1, days_in_year + 1)) {
277 // day of year roll-over, increment year
278 this->year++;
279 }
280}
281
282void ESPTime::recalc_timestamp_utc(bool use_day_of_year) {
283 time_t res = 0;
284 if (!this->fields_in_range(false, use_day_of_year)) {
285 this->timestamp = -1;
286 return;
287 }
288
289 for (int i = 1970; i < this->year; i++)
290 res += (i % 4 == 0) ? 366 : 365;
291
292 if (use_day_of_year) {
293 res += this->day_of_year - 1;
294 } else {
295 for (int i = 1; i < this->month; i++)
296 res += days_in_month(i, this->year);
297 res += this->day_of_month - 1;
298 }
299
300 res *= 24;
301 res += this->hour;
302 res *= 60;
303 res += this->minute;
304 res *= 60;
305 res += this->second;
306 this->timestamp = res;
307}
308
310#ifdef USE_TIME_TIMEZONE
311 // Calculate timestamp as if fields were UTC
312 this->recalc_timestamp_utc(false);
313 if (this->timestamp == -1) {
314 return; // Invalid time
315 }
316
317 // Now convert from local to UTC by adding the offset
318 // POSIX: local = utc - offset, so utc = local + offset
319 const auto &tz = time::get_global_tz();
320
321 if (!tz.has_dst()) {
322 // No DST - just apply standard offset
323 this->timestamp += tz.std_offset_seconds;
324 return;
325 }
326
327 // Try both interpretations to match libc mktime() with tm_isdst=-1
328 // For ambiguous times (fall-back repeated hour), prefer standard time
329 // For invalid times (spring-forward skipped hour), libc normalizes forward
330 time_t utc_if_dst = this->timestamp + tz.dst_offset_seconds;
331 time_t utc_if_std = this->timestamp + tz.std_offset_seconds;
332
333 bool dst_valid = time::is_in_dst(utc_if_dst, tz);
334 bool std_valid = !time::is_in_dst(utc_if_std, tz);
335
336 if (dst_valid && !std_valid) {
337 // Only DST interpretation is valid
338 this->timestamp = utc_if_dst;
339 } else {
340 // All other cases use standard offset:
341 // - Both valid (ambiguous fall-back repeated hour): prefer standard time
342 // - Only standard valid: straightforward
343 // - Neither valid (spring-forward skipped hour): std offset normalizes
344 // forward to match libc mktime(), e.g. 02:30 CST -> 03:30 CDT
345 this->timestamp = utc_if_std;
346 }
347#else
348 // No timezone support - treat as UTC
349 this->recalc_timestamp_utc(false);
350#endif
351}
352
354#ifdef USE_TIME_TIMEZONE
355 time_t now = ::time(nullptr);
356 const auto &tz = time::get_global_tz();
357 // POSIX offset is positive west, but we return offset to add to UTC to get local
358 // So we negate the POSIX offset
359 if (time::is_in_dst(now, tz)) {
360 return -tz.dst_offset_seconds;
361 }
362 return -tz.std_offset_seconds;
363#else
364 // No timezone support - no offset
365 return 0;
366#endif
367}
368
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; }
372bool ESPTime::operator>=(const ESPTime &other) const { return this->timestamp >= other.timestamp; }
373bool ESPTime::operator>(const ESPTime &other) const { return this->timestamp > other.timestamp; }
374
375template<typename T> bool increment_time_value(T &current, uint16_t begin, uint16_t end) {
376 current++;
377 if (current >= end) {
378 current = begin;
379 return true;
380 }
381 return false;
382}
383
384} // 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
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:17
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:455
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
const char int const __FlashStringHelper * format
Definition log.h:74
uint8_t days_in_month(uint8_t month, uint16_t year)
Definition time.cpp:11
std::string size_t len
Definition helpers.h:1045
bool increment_time_value(T &current, uint16_t begin, uint16_t end)
Definition time.cpp:375
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:236
static int32_t timezone_offset()
Definition time.cpp:353
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:282
bool operator<(const ESPTime &other) const
Definition time.cpp:369
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:373
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:264
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:139
bool operator<=(const ESPTime &other) const
Definition time.cpp:370
bool operator==(const ESPTime &other) const
Definition time.cpp:371
void recalc_timestamp_local()
Recalculate the timestamp field from the other fields of this ESPTime instance assuming local fields.
Definition time.cpp:309
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:372
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