Compare commits

..

9 Commits

Author SHA1 Message Date
J. Nick Koston
1688f9af0f Update esphome/components/usb_host/usb_host_client.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-12 10:42:38 -06:00
J. Nick Koston
8dd3021030 Merge branch 'dev' into usb-host-extract-cold-path 2026-02-12 10:37:51 -06:00
J. Nick Koston
cacd2b5fa3 Fix off-by-one in get_descriptor_string loop bound
bLength includes the 2-byte descriptor header, so the character count
is (bLength - 2) / 2, not bLength / 2. The old loop read one wData
entry past the actual string data. Also guard bLength < 2.
2026-02-12 10:35:42 -06:00
J. Nick Koston
60fef5e656 [analyze_memory] Fix mDNS packet buffer miscategorized as wifi_config (#13949)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 10:26:54 -06:00
J. Nick Koston
725e774fe7 [web_server] Guard icon JSON field with USE_ENTITY_ICON (#13948) 2026-02-12 10:26:36 -06:00
J. Nick Koston
9aa98ed6c6 [uart] Remove redundant mutex, fix flush race, conditional event queue (#13955) 2026-02-12 10:26:10 -06:00
J. Nick Koston
9c72c022b2 [usb_host] Extract cold path from loop(), replace std::string with buffer API
Extract the USB_CLIENT_OPEN state handling into handle_open_state_() to
keep loop() hot path small (~112 B vs 440 B before). Replace
get_descriptor_string() std::string return with caller-provided
fixed-size buffer via std::span to eliminate heap allocation during
device connection.
2026-02-12 10:25:53 -06:00
Guillermo Ruffino
7b251dcc31 [schema-gen] fix Windows: ensure UTF-8 encoding when reading component files (#13952) 2026-02-12 11:23:59 -05:00
schrob
8a08c688f6 [mipi_spi] Add Waveshare 1.83 v2 panel (#13680) 2026-02-12 23:25:51 +11:00
17 changed files with 307 additions and 2221 deletions

View File

@@ -256,7 +256,7 @@ SYMBOL_PATTERNS = {
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
# Order matters! More specific categories must come before general ones.
# mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern
"mdns_lib": ["mdns"],
"mdns_lib": ["mdns", "packet$"],
# memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols
"memory_mgmt": [
"mem_",
@@ -794,7 +794,6 @@ SYMBOL_PATTERNS = {
"s_dp",
"s_ni",
"s_reg_dump",
"packet$",
"d_mult_table",
"K",
"fcstab",

View File

@@ -1,4 +1,17 @@
from esphome.components.mipi import DriverChip
from esphome.components.mipi import (
ETMOD,
FRMCTR2,
GMCTRN1,
GMCTRP1,
IFCTR,
MODE_RGB,
PWCTR1,
PWCTR3,
PWCTR4,
PWCTR5,
PWSET,
DriverChip,
)
import esphome.config_validation as cv
from .amoled import CO5300
@@ -129,6 +142,16 @@ DriverChip(
),
),
)
ST7789P = DriverChip(
"ST7789P",
# Max supported dimensions
width=240,
height=320,
# SPI: RGB layout
color_order=MODE_RGB,
invert_colors=True,
draw_rounding=1,
)
ILI9488_A.extend(
"PICO-RESTOUCH-LCD-3.5",
@@ -162,3 +185,61 @@ AXS15231.extend(
cs_pin=9,
reset_pin=21,
)
# Waveshare 1.83-v2
#
# Do not use on 1.83-v1: Vendor warning on different chip!
ST7789P.extend(
"WAVESHARE-1.83-V2",
# Panel size smaller than ST7789 max allowed
width=240,
height=284,
# Vendor specific init derived from vendor sample code
# "LCD_1.83_Code_Rev2/ESP32/LCD_1in83/LCD_Driver.cpp"
# Compatible MIT license, see esphome/LICENSE file.
initsequence=(
(FRMCTR2, 0x0C, 0x0C, 0x00, 0x33, 0x33),
(ETMOD, 0x35),
(0xBB, 0x19),
(PWCTR1, 0x2C),
(PWCTR3, 0x01),
(PWCTR4, 0x12),
(PWCTR5, 0x20),
(IFCTR, 0x0F),
(PWSET, 0xA4, 0xA1),
(
GMCTRP1,
0xD0,
0x04,
0x0D,
0x11,
0x13,
0x2B,
0x3F,
0x54,
0x4C,
0x18,
0x0D,
0x0B,
0x1F,
0x23,
),
(
GMCTRN1,
0xD0,
0x04,
0x0C,
0x11,
0x13,
0x2C,
0x3F,
0x44,
0x51,
0x2F,
0x1F,
0x1F,
0x20,
0x23,
),
),
)

View File

@@ -1,481 +0,0 @@
#include "esphome/core/defines.h"
#ifdef USE_TIME_TIMEZONE
#include "posix_tz.h"
#include <cctype>
namespace esphome::time {
// Global timezone - set once at startup, rarely changes
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) - intentional mutable state
static ParsedTimezone global_tz_{};
void set_global_tz(const ParsedTimezone &tz) { global_tz_ = tz; }
const ParsedTimezone &get_global_tz() { return global_tz_; }
namespace internal {
// Helper to parse an unsigned integer from string, updating pointer
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)
static inline int days_in_year(int year) { return is_leap_year(year) ? 366 : 365; }
// Convert days since epoch to year, updating days to remainder
static int __attribute__((noinline)) days_to_year(int64_t &days) {
int year = 1970;
int diy;
while (days >= (diy = days_in_year(year))) {
days -= diy;
year++;
}
while (days < 0) {
year--;
days += days_in_year(year);
}
return year;
}
// Extract just the year from a UTC epoch
static int epoch_to_year(time_t epoch) {
int64_t days = epoch / 86400;
if (epoch < 0 && epoch % 86400 != 0)
days--;
return days_to_year(days);
}
int days_in_month(int year, int month) {
switch (month) {
case 2:
return is_leap_year(year) ? 29 : 28;
case 4:
case 6:
case 9:
case 11:
return 30;
default:
return 31;
}
}
// Zeller-like algorithm for day of week (0 = Sunday)
int __attribute__((noinline)) day_of_week(int year, int month, int day) {
// Adjust for January/February
if (month < 3) {
month += 12;
year--;
}
int k = year % 100;
int j = year / 100;
int h = (day + (13 * (month + 1)) / 5 + k + k / 4 + j / 4 - 2 * j) % 7;
// Convert from Zeller (0=Sat) to standard (0=Sun)
return ((h + 6) % 7);
}
void __attribute__((noinline)) epoch_to_tm_utc(time_t epoch, struct tm *out_tm) {
// Days since epoch
int64_t days = epoch / 86400;
int32_t remaining_secs = epoch % 86400;
if (remaining_secs < 0) {
days--;
remaining_secs += 86400;
}
out_tm->tm_sec = remaining_secs % 60;
remaining_secs /= 60;
out_tm->tm_min = remaining_secs % 60;
out_tm->tm_hour = remaining_secs / 60;
// Day of week (Jan 1, 1970 was Thursday = 4)
out_tm->tm_wday = static_cast<int>((days + 4) % 7);
if (out_tm->tm_wday < 0)
out_tm->tm_wday += 7;
// Calculate year (updates days to day-of-year)
int year = days_to_year(days);
out_tm->tm_year = year - 1900;
out_tm->tm_yday = static_cast<int>(days);
// Calculate month and day
int month = 1;
int dim;
while (days >= (dim = days_in_month(year, month))) {
days -= dim;
month++;
}
out_tm->tm_mon = month - 1;
out_tm->tm_mday = static_cast<int>(days) + 1;
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
// Iterate forward through months (no array needed)
int remaining = julian_day;
out_month = 1;
while (out_month <= 12) {
// Days in month for non-leap year (J format ignores leap years)
int dim = days_in_month(2001, out_month); // 2001 is non-leap year
if (remaining <= dim) {
out_day = remaining;
return;
}
remaining -= dim;
out_month++;
}
out_day = remaining;
}
void __attribute__((noinline)) day_of_year_to_month_day(int day_of_year, int year, int &out_month, int &out_day) {
// Plain format: day 0-365, Feb 29 IS counted in leap years
// Day 0 = Jan 1
int remaining = day_of_year;
out_month = 1;
while (out_month <= 12) {
int days_this_month = days_in_month(year, out_month);
if (remaining < days_this_month) {
out_day = remaining + 1;
return;
}
remaining -= days_this_month;
out_month++;
}
// Shouldn't reach here with valid input
out_month = 12;
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;
for (int m = 1; m < month; m++) {
days += days_in_month(year, m);
}
return days;
}
// Calculate days from epoch to Jan 1 of given year (for DST transition calculations)
// Only supports years >= 1970. Timezone is either compiled in from YAML or set by
// Home Assistant, so pre-1970 dates are not a concern.
static int64_t __attribute__((noinline)) days_to_year_start(int year) {
int64_t days = 0;
for (int y = 1970; y < year; y++) {
days += days_in_year(y);
}
return days;
}
time_t __attribute__((noinline)) calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds) {
int month, day;
switch (rule.type) {
case DSTRuleType::MONTH_WEEK_DAY: {
// Find the nth occurrence of day_of_week in the given month
int first_dow = day_of_week(year, rule.month, 1);
// Days until first occurrence of target day
int days_until_first = (rule.day_of_week - first_dow + 7) % 7;
int first_occurrence = 1 + days_until_first;
if (rule.week == 5) {
// "Last" occurrence - find the last one in the month
int dim = days_in_month(year, rule.month);
day = first_occurrence;
while (day + 7 <= dim) {
day += 7;
}
} else {
// nth occurrence
day = first_occurrence + (rule.week - 1) * 7;
}
month = rule.month;
break;
}
case DSTRuleType::JULIAN_NO_LEAP:
// J format: day 1-365, Feb 29 not counted
julian_to_month_day(rule.day, month, day);
break;
case DSTRuleType::DAY_OF_YEAR:
// Plain format: day 0-365, Feb 29 counted
day_of_year_to_month_day(rule.day, year, month, day);
break;
case DSTRuleType::NONE:
// Should never be called with NONE, but handle it gracefully
month = 1;
day = 1;
break;
}
// Calculate days from epoch to this date
int64_t days = days_to_year_start(year) + days_from_year_start(year, month, day);
// Convert to epoch and add transition time and base offset
return days * 86400 + rule.time_seconds + base_offset_seconds;
}
} // namespace internal
bool __attribute__((noinline)) is_in_dst(time_t utc_epoch, const ParsedTimezone &tz) {
if (!tz.has_dst()) {
return false;
}
int year = internal::epoch_to_year(utc_epoch);
// Calculate DST start and end for this year
// DST start transition happens in standard time
time_t dst_start = internal::calculate_dst_transition(year, tz.dst_start, tz.std_offset_seconds);
// DST end transition happens in daylight time
time_t dst_end = internal::calculate_dst_transition(year, tz.dst_end, tz.dst_offset_seconds);
if (dst_start < dst_end) {
// Northern hemisphere: DST is between start and end
return (utc_epoch >= dst_start && utc_epoch < dst_end);
} else {
// Southern hemisphere: DST is outside the range (wraps around year)
return (utc_epoch >= dst_start || utc_epoch < dst_end);
}
}
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;
}
// Determine DST status once (avoids duplicate is_in_dst calculation)
bool in_dst = is_in_dst(utc_epoch, tz);
int32_t offset = in_dst ? tz.dst_offset_seconds : tz.std_offset_seconds;
// Apply offset (POSIX offset is positive west, so subtract to get local)
time_t local_epoch = utc_epoch - offset;
internal::epoch_to_tm_utc(local_epoch, out_tm);
out_tm->tm_isdst = in_dst ? 1 : 0;
return true;
}
} // namespace esphome::time
#ifndef USE_HOST
// Override libc's localtime functions to use our timezone on embedded platforms.
// This allows user lambdas calling ::localtime() to get correct local time
// without needing the TZ environment variable (which pulls in scanf bloat).
// On host, we use the normal TZ mechanism since there's no memory constraint.
// Thread-safe version
extern "C" struct tm *localtime_r(const time_t *timer, struct tm *result) {
if (timer == nullptr || result == nullptr) {
return nullptr;
}
esphome::time::epoch_to_local_tm(*timer, esphome::time::get_global_tz(), result);
return result;
}
// Non-thread-safe version (uses static buffer, standard libc behavior)
extern "C" struct tm *localtime(const time_t *timer) {
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static struct tm localtime_buf;
return localtime_r(timer, &localtime_buf);
}
#endif // !USE_HOST
#endif // USE_TIME_TIMEZONE

View File

@@ -1,132 +0,0 @@
#pragma once
#ifdef USE_TIME_TIMEZONE
#include <cstdint>
#include <ctime>
namespace esphome::time {
/// Type of DST transition rule
enum class DSTRuleType : uint8_t {
NONE = 0, ///< No DST rule (used to indicate no DST)
MONTH_WEEK_DAY, ///< M format: Mm.w.d (e.g., M3.2.0 = 2nd Sunday of March)
JULIAN_NO_LEAP, ///< J format: Jn (day 1-365, Feb 29 not counted)
DAY_OF_YEAR, ///< Plain number: n (day 0-365, Feb 29 counted in leap years)
};
/// Rule for DST transition (packed for 32-bit: 12 bytes)
struct DSTRule {
int32_t time_seconds; ///< Seconds after midnight (default 7200 = 2:00 AM)
uint16_t day; ///< Day of year (for JULIAN_NO_LEAP and DAY_OF_YEAR)
DSTRuleType type; ///< Type of rule
uint8_t month; ///< Month 1-12 (for MONTH_WEEK_DAY)
uint8_t week; ///< Week 1-5, 5 = last (for MONTH_WEEK_DAY)
uint8_t day_of_week; ///< Day 0-6, 0 = Sunday (for MONTH_WEEK_DAY)
};
/// Parsed POSIX timezone information (packed for 32-bit: 32 bytes)
struct ParsedTimezone {
int32_t std_offset_seconds; ///< Standard time offset from UTC in seconds (positive = west)
int32_t dst_offset_seconds; ///< DST offset from UTC in seconds
DSTRule dst_start; ///< When DST starts
DSTRule dst_end; ///< When DST ends
/// Check if this timezone has DST rules
bool has_dst() const { return this->dst_start.type != DSTRuleType::NONE; }
};
/// Parse a POSIX TZ string into a ParsedTimezone struct.
/// 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
/// @param tz The parsed timezone
/// @param[out] out_tm Output tm struct with local time
/// @return true on success
bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm);
/// Set the global timezone used by epoch_to_local_tm() when called without a timezone.
/// This is called by RealTimeClock::apply_timezone_() to enable ESPTime::from_epoch_local()
/// to work without libc's localtime().
void set_global_tz(const ParsedTimezone &tz);
/// Get the global timezone.
const ParsedTimezone &get_global_tz();
/// Check if a given UTC epoch falls within DST for the parsed timezone.
/// @param utc_epoch Unix timestamp in UTC
/// @param tz The parsed timezone
/// @return true if DST is in effect at the given time
bool is_in_dst(time_t utc_epoch, const ParsedTimezone &tz);
// Internal helper functions exposed for testing
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
/// @param[out] day Output: day of month
void julian_to_month_day(int julian_day, int &month, int &day);
/// Convert day of year (plain format, 0-365 counting Feb 29) to month/day
/// @param day_of_year Day number 0-365
/// @param year The year (for leap year calculation)
/// @param[out] month Output: month 1-12
/// @param[out] day Output: day of month
void day_of_year_to_month_day(int day_of_year, int year, int &month, int &day);
/// Calculate day of week for any date (0 = Sunday)
/// Uses a simplified algorithm that works for years 1970-2099
int day_of_week(int year, int month, int day);
/// Get the number of days in a month
int days_in_month(int year, int month);
/// Check if a year is a leap year
bool is_leap_year(int year);
/// Convert epoch to year/month/day/hour/min/sec (UTC)
void epoch_to_tm_utc(time_t epoch, struct tm *out_tm);
/// Calculate the epoch timestamp for a DST transition in a given year.
/// @param year The year (e.g., 2026)
/// @param rule The DST rule (month, week, day_of_week, time)
/// @param base_offset_seconds The timezone offset to apply (std or dst depending on context)
/// @return Unix epoch timestamp of the transition
time_t calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds);
} // namespace internal
} // namespace esphome::time
#endif // USE_TIME_TIMEZONE

View File

@@ -14,8 +14,8 @@
#include <sys/time.h>
#endif
#include <cerrno>
#include <cinttypes>
#include <cstdlib>
namespace esphome::time {
@@ -23,33 +23,9 @@ static const char *const TAG = "time";
RealTimeClock::RealTimeClock() = default;
ESPTime __attribute__((noinline)) RealTimeClock::now() {
#ifdef USE_TIME_TIMEZONE
time_t epoch = this->timestamp_now();
struct tm local_tm;
if (epoch_to_local_tm(epoch, get_global_tz(), &local_tm)) {
return ESPTime::from_c_tm(&local_tm, epoch);
}
// Fallback to UTC if parsing failed
return ESPTime::from_epoch_utc(epoch);
#else
return ESPTime::from_epoch_local(this->timestamp_now());
#endif
}
void RealTimeClock::dump_config() {
#ifdef USE_TIME_TIMEZONE
const auto &tz = get_global_tz();
// POSIX offset is positive west, negate for conventional UTC+X display
int std_h = -tz.std_offset_seconds / 3600;
int std_m = (std::abs(tz.std_offset_seconds) % 3600) / 60;
if (tz.has_dst()) {
int dst_h = -tz.dst_offset_seconds / 3600;
int dst_m = (std::abs(tz.dst_offset_seconds) % 3600) / 60;
ESP_LOGCONFIG(TAG, "Timezone: UTC%+d:%02d (DST UTC%+d:%02d)", std_h, std_m, dst_h, dst_m);
} else {
ESP_LOGCONFIG(TAG, "Timezone: UTC%+d:%02d", std_h, std_m);
}
ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str());
#endif
auto time = this->now();
ESP_LOGCONFIG(TAG, "Current time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour,
@@ -96,6 +72,11 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
ret = settimeofday(&timev, nullptr);
}
#ifdef USE_TIME_TIMEZONE
// Move timezone back to local timezone.
this->apply_timezone_();
#endif
if (ret != 0) {
ESP_LOGW(TAG, "setimeofday() failed with code %d", ret);
}
@@ -108,29 +89,9 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
}
#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);
void RealTimeClock::apply_timezone_() {
setenv("TZ", this->timezone_.c_str(), 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

View File

@@ -6,9 +6,6 @@
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/time.h"
#ifdef USE_TIME_TIMEZONE
#include "posix_tz.h"
#endif
namespace esphome::time {
@@ -23,31 +20,26 @@ class RealTimeClock : public PollingComponent {
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.
void set_timezone(const std::string &tz) {
this->timezone_ = tz;
this->apply_timezone_();
}
/// Set the time zone from a std::string.
void set_timezone(const std::string &tz) { this->apply_timezone_(tz.c_str()); }
/// Set the time zone from raw buffer, only if it differs from the current one.
void set_timezone(const char *tz, size_t len) {
if (this->timezone_.length() != len || memcmp(this->timezone_.c_str(), tz, len) != 0) {
this->timezone_.assign(tz, len);
this->apply_timezone_();
}
}
/// Get the time zone currently in use.
std::string get_timezone() { return this->timezone_; }
#endif
/// Get the time in the currently defined timezone.
ESPTime now();
ESPTime now() { return ESPTime::from_epoch_local(this->timestamp_now()); }
/// Get the time without any time zone or DST corrections.
ESPTime utcnow() { return ESPTime::from_epoch_utc(this->timestamp_now()); }
@@ -66,7 +58,8 @@ class RealTimeClock : public PollingComponent {
void synchronize_epoch_(uint32_t epoch);
#ifdef USE_TIME_TIMEZONE
void apply_timezone_(const char *tz);
std::string timezone_{};
void apply_timezone_();
#endif
LazyCallbackManager<void()> time_sync_callback_;

View File

@@ -90,7 +90,6 @@ void IDFUARTComponent::setup() {
return;
}
this->uart_num_ = static_cast<uart_port_t>(next_uart_num++);
this->lock_ = xSemaphoreCreateMutex();
#if (SOC_UART_LP_NUM >= 1)
size_t fifo_len = ((this->uart_num_ < SOC_UART_HP_NUM) ? SOC_UART_FIFO_LEN : SOC_LP_UART_FIFO_LEN);
@@ -102,11 +101,7 @@ void IDFUARTComponent::setup() {
this->rx_buffer_size_ = fifo_len * 2;
}
xSemaphoreTake(this->lock_, portMAX_DELAY);
this->load_settings(false);
xSemaphoreGive(this->lock_);
}
void IDFUARTComponent::load_settings(bool dump_config) {
@@ -126,13 +121,20 @@ void IDFUARTComponent::load_settings(bool dump_config) {
return;
}
}
#ifdef USE_UART_WAKE_LOOP_ON_RX
constexpr int event_queue_size = 20;
QueueHandle_t *event_queue_ptr = &this->uart_event_queue_;
#else
constexpr int event_queue_size = 0;
QueueHandle_t *event_queue_ptr = nullptr;
#endif
err = uart_driver_install(this->uart_num_, // UART number
this->rx_buffer_size_, // RX ring buffer size
0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will
// block task until all data has been sent out
20, // event queue size/depth
&this->uart_event_queue_, // event queue
0 // Flags used to allocate the interrupt
0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will
// block task until all data has been sent out
event_queue_size, // event queue size/depth
event_queue_ptr, // event queue
0 // Flags used to allocate the interrupt
);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
@@ -282,9 +284,7 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) {
}
void IDFUARTComponent::write_array(const uint8_t *data, size_t len) {
xSemaphoreTake(this->lock_, portMAX_DELAY);
int32_t write_len = uart_write_bytes(this->uart_num_, data, len);
xSemaphoreGive(this->lock_);
if (write_len != (int32_t) len) {
ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len);
this->mark_failed();
@@ -299,7 +299,6 @@ void IDFUARTComponent::write_array(const uint8_t *data, size_t len) {
bool IDFUARTComponent::peek_byte(uint8_t *data) {
if (!this->check_read_timeout_())
return false;
xSemaphoreTake(this->lock_, portMAX_DELAY);
if (this->has_peek_) {
*data = this->peek_byte_;
} else {
@@ -311,7 +310,6 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) {
this->peek_byte_ = *data;
}
}
xSemaphoreGive(this->lock_);
return true;
}
@@ -320,7 +318,6 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
int32_t read_len = 0;
if (!this->check_read_timeout_(len))
return false;
xSemaphoreTake(this->lock_, portMAX_DELAY);
if (this->has_peek_) {
length_to_read--;
*data = this->peek_byte_;
@@ -329,7 +326,6 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
}
if (length_to_read > 0)
read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS);
xSemaphoreGive(this->lock_);
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
@@ -342,9 +338,7 @@ size_t IDFUARTComponent::available() {
size_t available = 0;
esp_err_t err;
xSemaphoreTake(this->lock_, portMAX_DELAY);
err = uart_get_buffered_data_len(this->uart_num_, &available);
xSemaphoreGive(this->lock_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_get_buffered_data_len failed: %s", esp_err_to_name(err));
@@ -358,9 +352,7 @@ size_t IDFUARTComponent::available() {
void IDFUARTComponent::flush() {
ESP_LOGVV(TAG, " Flushing");
xSemaphoreTake(this->lock_, portMAX_DELAY);
uart_wait_tx_done(this->uart_num_, portMAX_DELAY);
xSemaphoreGive(this->lock_);
}
void IDFUARTComponent::check_logger_conflict() {}
@@ -384,6 +376,13 @@ void IDFUARTComponent::start_rx_event_task_() {
ESP_LOGV(TAG, "RX event task started");
}
// FreeRTOS task that relays UART ISR events to the main loop.
// This task exists because wake_loop_threadsafe() is not ISR-safe (it uses a
// UDP loopback socket), so we need a task as an ISR-to-main-loop trampoline.
// IMPORTANT: This task must NOT call any UART wrapper methods (read_array,
// write_array, peek_byte, etc.) or touch has_peek_/peek_byte_ — all reading
// is done by the main loop. This task only reads from the event queue and
// calls App.wake_loop_threadsafe().
void IDFUARTComponent::rx_event_task_func(void *param) {
auto *self = static_cast<IDFUARTComponent *>(param);
uart_event_t event;
@@ -405,8 +404,14 @@ void IDFUARTComponent::rx_event_task_func(void *param) {
case UART_FIFO_OVF:
case UART_BUFFER_FULL:
ESP_LOGW(TAG, "FIFO overflow or ring buffer full - clearing");
uart_flush_input(self->uart_num_);
// Don't call uart_flush_input() here — this task does not own the read side.
// ESP-IDF examples flush on overflow because the same task handles both events
// and reads, so flush and read are serialized. Here, reads happen on the main
// loop, so flushing from this task races with read_array() and can destroy data
// mid-read. The driver self-heals without an explicit flush: uart_read_bytes()
// calls uart_check_buf_full() after each chunk, which moves stashed FIFO bytes
// into the ring buffer and re-enables RX interrupts once space is freed.
ESP_LOGW(TAG, "FIFO overflow or ring buffer full");
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif

View File

@@ -8,6 +8,13 @@
namespace esphome::uart {
/// ESP-IDF UART driver wrapper.
///
/// Thread safety: All public methods must only be called from the main loop.
/// The ESP-IDF UART driver API does not guarantee thread safety, and ESPHome's
/// peek byte state (has_peek_/peek_byte_) is not synchronized. The rx_event_task
/// (when enabled) must not call any of these methods — it communicates with the
/// main loop exclusively via App.wake_loop_threadsafe().
class IDFUARTComponent : public UARTComponent, public Component {
public:
void setup() override;
@@ -26,7 +33,9 @@ class IDFUARTComponent : public UARTComponent, public Component {
void flush() override;
uint8_t get_hw_serial_number() { return this->uart_num_; }
#ifdef USE_UART_WAKE_LOOP_ON_RX
QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; }
#endif
/**
* Load the UART with the current settings.
@@ -46,18 +55,20 @@ class IDFUARTComponent : public UARTComponent, public Component {
protected:
void check_logger_conflict() override;
uart_port_t uart_num_;
QueueHandle_t uart_event_queue_;
uart_config_t get_config_();
SemaphoreHandle_t lock_;
bool has_peek_{false};
uint8_t peek_byte_;
#ifdef USE_UART_WAKE_LOOP_ON_RX
// RX notification support
// RX notification support — runs on a separate FreeRTOS task.
// IMPORTANT: rx_event_task_func must NOT call any UART wrapper methods (read_array,
// write_array, etc.) or touch has_peek_/peek_byte_. It must only read from the
// event queue and call App.wake_loop_threadsafe().
void start_rx_event_task_();
static void rx_event_task_func(void *param);
QueueHandle_t uart_event_queue_;
TaskHandle_t rx_event_task_handle_{nullptr};
#endif // USE_UART_WAKE_LOOP_ON_RX
};

View File

@@ -148,6 +148,7 @@ class USBClient : public Component {
EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool;
protected:
void handle_open_state_();
TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe)
virtual void disconnect();
virtual void on_connected() {}

View File

@@ -9,6 +9,7 @@
#include <cinttypes>
#include <cstring>
#include <atomic>
#include <span>
namespace esphome {
namespace usb_host {
@@ -142,18 +143,23 @@ static void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc
} while (next_desc != NULL);
}
#endif
static std::string get_descriptor_string(const usb_str_desc_t *desc) {
char buffer[256];
if (desc == nullptr)
// USB string descriptors: bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType).
// Character count = (bLength - 2) / 2, max 126 chars + null terminator.
static constexpr size_t DESC_STRING_BUF_SIZE = 128;
static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer) {
if (desc == nullptr || desc->bLength < 2)
return "(unspecified)";
char *p = buffer;
for (int i = 0; i != desc->bLength / 2; i++) {
int char_count = (desc->bLength - 2) / 2;
char *p = buffer.data();
char *end = p + buffer.size() - 1;
for (int i = 0; i != char_count && p < end; i++) {
auto c = desc->wData[i];
if (c < 0x100)
*p++ = static_cast<char>(c);
}
*p = '\0';
return {buffer};
return buffer.data();
}
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
@@ -259,60 +265,63 @@ void USBClient::loop() {
ESP_LOGW(TAG, "Dropped %u USB events due to queue overflow", dropped);
}
switch (this->state_) {
case USB_CLIENT_OPEN: {
int err;
ESP_LOGD(TAG, "Open device %d", this->device_addr_);
err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err));
this->state_ = USB_CLIENT_INIT;
break;
}
ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_);
const usb_device_desc_t *desc;
err = usb_host_get_device_descriptor(this->device_handle_, &desc);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err));
this->disconnect();
} else {
ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct);
if (desc->idVendor == this->vid_ && desc->idProduct == this->pid_ || this->vid_ == 0 && this->pid_ == 0) {
usb_device_info_t dev_info;
err = usb_host_device_info(this->device_handle_, &dev_info);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err));
this->disconnect();
break;
}
this->state_ = USB_CLIENT_CONNECTED;
ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s",
get_descriptor_string(dev_info.str_desc_manufacturer).c_str(),
get_descriptor_string(dev_info.str_desc_product).c_str(),
get_descriptor_string(dev_info.str_desc_serial_num).c_str());
if (this->state_ == USB_CLIENT_OPEN) {
this->handle_open_state_();
}
}
void USBClient::handle_open_state_() {
int err;
ESP_LOGD(TAG, "Open device %d", this->device_addr_);
err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err));
this->state_ = USB_CLIENT_INIT;
return;
}
ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_);
const usb_device_desc_t *desc;
err = usb_host_get_device_descriptor(this->device_handle_, &desc);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err));
this->disconnect();
return;
}
ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct);
if (desc->idVendor != this->vid_ || desc->idProduct != this->pid_) {
if (this->vid_ != 0 || this->pid_ != 0) {
ESP_LOGD(TAG, "Not our device, closing");
this->disconnect();
return;
}
}
usb_device_info_t dev_info;
err = usb_host_device_info(this->device_handle_, &dev_info);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err));
this->disconnect();
return;
}
this->state_ = USB_CLIENT_CONNECTED;
char buf_manuf[DESC_STRING_BUF_SIZE];
char buf_product[DESC_STRING_BUF_SIZE];
char buf_serial[DESC_STRING_BUF_SIZE];
ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s",
get_descriptor_string(dev_info.str_desc_manufacturer, buf_manuf),
get_descriptor_string(dev_info.str_desc_product, buf_product),
get_descriptor_string(dev_info.str_desc_serial_num, buf_serial));
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
const usb_device_desc_t *device_desc;
err = usb_host_get_device_descriptor(this->device_handle_, &device_desc);
if (err == ESP_OK)
usb_client_print_device_descriptor(device_desc);
const usb_config_desc_t *config_desc;
err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc);
if (err == ESP_OK)
usb_client_print_config_descriptor(config_desc, nullptr);
const usb_device_desc_t *device_desc;
err = usb_host_get_device_descriptor(this->device_handle_, &device_desc);
if (err == ESP_OK)
usb_client_print_device_descriptor(device_desc);
const usb_config_desc_t *config_desc;
err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc);
if (err == ESP_OK)
usb_client_print_config_descriptor(config_desc, nullptr);
#endif
this->on_connected();
} else {
ESP_LOGD(TAG, "Not our device, closing");
this->disconnect();
}
}
break;
}
default:
break;
}
this->on_connected();
}
void USBClient::on_opened(uint8_t addr) {

View File

@@ -557,7 +557,9 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J
root[ESPHOME_F("device")] = device_name;
}
#endif
#ifdef USE_ENTITY_ICON
root[ESPHOME_F("icon")] = obj->get_icon_ref();
#endif
root[ESPHOME_F("entity_category")] = obj->get_entity_category();
bool is_disabled = obj->is_disabled_by_default();
if (is_disabled)

View File

@@ -2,6 +2,7 @@
#include "helpers.h"
#include <algorithm>
#include <cinttypes>
namespace esphome {
@@ -66,121 +67,56 @@ std::string ESPTime::strftime(const char *format) {
std::string ESPTime::strftime(const std::string &format) { return this->strftime(format.c_str()); }
// Helper to parse exactly N digits, returns false if not enough digits
static bool parse_digits(const char *&p, const char *end, int count, uint16_t &value) {
value = 0;
for (int i = 0; i < count; i++) {
if (p >= end || *p < '0' || *p > '9')
return false;
value = value * 10 + (*p - '0');
p++;
}
return true;
}
// Helper to check for expected character
static bool expect_char(const char *&p, const char *end, char expected) {
if (p >= end || *p != expected)
return false;
p++;
return true;
}
bool ESPTime::strptime(const char *time_to_parse, size_t len, ESPTime &esp_time) {
// Supported formats:
// YYYY-MM-DD HH:MM:SS (19 chars)
// YYYY-MM-DD HH:MM (16 chars)
// YYYY-MM-DD (10 chars)
// HH:MM:SS (8 chars)
// HH:MM (5 chars)
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
int num;
const int ilen = static_cast<int>(len);
if (time_to_parse == nullptr || len == 0)
if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %02hhu:%02hhu:%02hhu %n", &year, &month, &day, // NOLINT
&hour, // NOLINT
&minute, // NOLINT
&second, &num) == 6 && // NOLINT
num == ilen) {
esp_time.year = year;
esp_time.month = month;
esp_time.day_of_month = day;
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = second;
} else if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %02hhu:%02hhu %n", &year, &month, &day, // NOLINT
&hour, // NOLINT
&minute, &num) == 5 && // NOLINT
num == ilen) {
esp_time.year = year;
esp_time.month = month;
esp_time.day_of_month = day;
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = 0;
} else if (sscanf(time_to_parse, "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT
num == ilen) {
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = second;
} else if (sscanf(time_to_parse, "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT
num == ilen) {
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = 0;
} else if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT
num == ilen) {
esp_time.year = year;
esp_time.month = month;
esp_time.day_of_month = day;
} else {
return false;
const char *p = time_to_parse;
const char *end = time_to_parse + len;
uint16_t v1, v2, v3, v4, v5, v6;
// Try date formats first (start with 4-digit year)
if (len >= 10 && time_to_parse[4] == '-') {
// YYYY-MM-DD...
if (!parse_digits(p, end, 4, v1))
return false;
if (!expect_char(p, end, '-'))
return false;
if (!parse_digits(p, end, 2, v2))
return false;
if (!expect_char(p, end, '-'))
return false;
if (!parse_digits(p, end, 2, v3))
return false;
esp_time.year = v1;
esp_time.month = v2;
esp_time.day_of_month = v3;
if (p == end) {
// YYYY-MM-DD (date only)
return true;
}
if (!expect_char(p, end, ' '))
return false;
// Continue with time part: HH:MM[:SS]
if (!parse_digits(p, end, 2, v4))
return false;
if (!expect_char(p, end, ':'))
return false;
if (!parse_digits(p, end, 2, v5))
return false;
esp_time.hour = v4;
esp_time.minute = v5;
if (p == end) {
// YYYY-MM-DD HH:MM
esp_time.second = 0;
return true;
}
if (!expect_char(p, end, ':'))
return false;
if (!parse_digits(p, end, 2, v6))
return false;
esp_time.second = v6;
return p == end; // YYYY-MM-DD HH:MM:SS
}
// Try time-only formats (HH:MM[:SS])
if (len >= 5 && time_to_parse[2] == ':') {
if (!parse_digits(p, end, 2, v1))
return false;
if (!expect_char(p, end, ':'))
return false;
if (!parse_digits(p, end, 2, v2))
return false;
esp_time.hour = v1;
esp_time.minute = v2;
if (p == end) {
// HH:MM
esp_time.second = 0;
return true;
}
if (!expect_char(p, end, ':'))
return false;
if (!parse_digits(p, end, 2, v3))
return false;
esp_time.second = v3;
return p == end; // HH:MM:SS
}
return false;
return true;
}
void ESPTime::increment_second() {
@@ -257,67 +193,27 @@ void ESPTime::recalc_timestamp_utc(bool use_day_of_year) {
}
void ESPTime::recalc_timestamp_local() {
#ifdef USE_TIME_TIMEZONE
// Calculate timestamp as if fields were UTC
this->recalc_timestamp_utc(false);
if (this->timestamp == -1) {
return; // Invalid time
}
struct tm tm;
// Now convert from local to UTC by adding the offset
// POSIX: local = utc - offset, so utc = local + offset
const auto &tz = time::get_global_tz();
tm.tm_year = this->year - 1900;
tm.tm_mon = this->month - 1;
tm.tm_mday = this->day_of_month;
tm.tm_hour = this->hour;
tm.tm_min = this->minute;
tm.tm_sec = this->second;
tm.tm_isdst = -1;
if (!tz.has_dst()) {
// No DST - just apply standard offset
this->timestamp += tz.std_offset_seconds;
return;
}
// Try both interpretations to match libc mktime() with tm_isdst=-1
// For ambiguous times (fall-back repeated hour), prefer standard time
// For invalid times (spring-forward skipped hour), libc normalizes forward
time_t utc_if_dst = this->timestamp + tz.dst_offset_seconds;
time_t utc_if_std = this->timestamp + tz.std_offset_seconds;
bool dst_valid = time::is_in_dst(utc_if_dst, tz);
bool std_valid = !time::is_in_dst(utc_if_std, tz);
if (dst_valid && std_valid) {
// Ambiguous time (repeated hour during fall-back) - prefer standard time
this->timestamp = utc_if_std;
} else if (dst_valid) {
// Only DST interpretation is valid
this->timestamp = utc_if_dst;
} else if (std_valid) {
// Only standard interpretation is valid
this->timestamp = utc_if_std;
} else {
// Invalid time (skipped hour during spring-forward)
// libc normalizes forward: 02:30 CST -> 08:30 UTC -> 03:30 CDT
// Using std offset achieves this since the UTC result falls during DST
this->timestamp = utc_if_std;
}
#else
// No timezone support - treat as UTC
this->recalc_timestamp_utc(false);
#endif
this->timestamp = mktime(&tm);
}
int32_t ESPTime::timezone_offset() {
#ifdef USE_TIME_TIMEZONE
time_t now = ::time(nullptr);
const auto &tz = time::get_global_tz();
// POSIX offset is positive west, but we return offset to add to UTC to get local
// So we negate the POSIX offset
if (time::is_in_dst(now, tz)) {
return -tz.dst_offset_seconds;
}
return -tz.std_offset_seconds;
#else
// No timezone support - no offset
return 0;
#endif
struct tm local_tm = *::localtime(&now);
local_tm.tm_isdst = 0; // Cause mktime to ignore daylight saving time because we want to include it in the offset.
time_t local_time = mktime(&local_tm);
struct tm utc_tm = *::gmtime(&now);
time_t utc_time = mktime(&utc_tm);
return static_cast<int32_t>(local_time - utc_time);
}
bool ESPTime::operator<(const ESPTime &other) const { return this->timestamp < other.timestamp; }

View File

@@ -7,10 +7,6 @@
#include <span>
#include <string>
#ifdef USE_TIME_TIMEZONE
#include "esphome/components/time/posix_tz.h"
#endif
namespace esphome {
template<typename T> bool increment_time_value(T &current, uint16_t begin, uint16_t end);
@@ -109,17 +105,11 @@ struct ESPTime {
* @return The generated ESPTime
*/
static ESPTime from_epoch_local(time_t epoch) {
#ifdef USE_TIME_TIMEZONE
struct tm local_tm;
if (time::epoch_to_local_tm(epoch, time::get_global_tz(), &local_tm)) {
return ESPTime::from_c_tm(&local_tm, epoch);
struct tm *c_tm = ::localtime(&epoch);
if (c_tm == nullptr) {
return ESPTime{}; // Return an invalid ESPTime
}
// Fallback to UTC if conversion failed
return ESPTime::from_epoch_utc(epoch);
#else
// No timezone support - return UTC (no TZ configured, localtime would return UTC anyway)
return ESPTime::from_epoch_utc(epoch);
#endif
return ESPTime::from_c_tm(c_tm, epoch);
}
/** Convert an UTC epoch timestamp to a UTC time ESPTime instance.
*

View File

@@ -369,7 +369,7 @@ def get_logger_tags():
"api.service",
]
for file in CORE_COMPONENTS_PATH.rglob("*.cpp"):
data = file.read_text()
data = file.read_text(encoding="utf-8")
match = pattern.search(data)
if match:
tags.append(match.group(1))

View File

@@ -66,7 +66,6 @@ def create_test_config(config_name: str, includes: list[str]) -> dict:
],
"build_flags": [
"-Og", # optimize for debug
"-DUSE_TIME_TIMEZONE", # enable timezone code paths for testing
],
"debug_build_flags": [ # only for debug builds
"-g3", # max debug info

View File

@@ -3,9 +3,15 @@ display:
spi_16: true
pixel_mode: 18bit
model: ili9488
dc_pin: ${dc_pin}
cs_pin: ${cs_pin}
reset_pin: ${reset_pin}
dc_pin:
allow_other_uses: true
number: ${dc_pin}
cs_pin:
allow_other_uses: true
number: ${cs_pin}
reset_pin:
allow_other_uses: true
number: ${reset_pin}
data_rate: 20MHz
invert_colors: true
show_test_card: true
@@ -24,3 +30,15 @@ display:
height: 200
enable_pin: ${enable_pin}
bus_mode: single
- platform: mipi_spi
model: WAVESHARE-1.83-V2
dc_pin:
allow_other_uses: true
number: ${dc_pin}
cs_pin:
allow_other_uses: true
number: ${cs_pin}
reset_pin:
allow_other_uses: true
number: ${reset_pin}

File diff suppressed because it is too large Load Diff