From b4817c424d6a71b2b01b0d5121681ceea8f28c6d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 16:49:18 -0600 Subject: [PATCH 1/3] [api] Skip timezone update when parsed struct is not populated Old clients (before 2026.3.0) send only the timezone string without the parsed_timezone struct, so all fields default to zero. Without this check, the device would overwrite its codegen-configured timezone with UTC. Keep the codegen timezone when the struct is unpopulated (all zeros). For actual UTC this also skips, which is harmless since UTC is the default. --- esphome/components/api/api_connection.cpp | 41 +++++++++++++---------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index bd62b4e31d..394c01bcab 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1112,24 +1112,31 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { if (homeassistant::global_homeassistant_time != nullptr) { homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds); #ifdef USE_TIME_TIMEZONE - if (!value.timezone.empty()) { + // Only apply if the sender provided pre-parsed timezone data. + // Old clients (before 2026.3.0) only send the timezone string without the parsed struct, + // so all parsed_timezone fields default to zero — skip to keep the codegen-configured timezone. + // For actual UTC (all zeros), this also skips, which is harmless since UTC is the default. + // Eventually the timezone string will be removed and only the struct will be sent. + { const auto &pt = value.parsed_timezone; - 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(pt.dst_start.day); - tz.dst_start.type = static_cast(pt.dst_start.type); - tz.dst_start.month = static_cast(pt.dst_start.month); - tz.dst_start.week = static_cast(pt.dst_start.week); - tz.dst_start.day_of_week = static_cast(pt.dst_start.day_of_week); - tz.dst_end.time_seconds = pt.dst_end.time_seconds; - tz.dst_end.day = static_cast(pt.dst_end.day); - tz.dst_end.type = static_cast(pt.dst_end.type); - tz.dst_end.month = static_cast(pt.dst_end.month); - tz.dst_end.week = static_cast(pt.dst_end.week); - tz.dst_end.day_of_week = static_cast(pt.dst_end.day_of_week); - time::set_global_tz(tz); + 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(pt.dst_start.day); + tz.dst_start.type = static_cast(pt.dst_start.type); + tz.dst_start.month = static_cast(pt.dst_start.month); + tz.dst_start.week = static_cast(pt.dst_start.week); + tz.dst_start.day_of_week = static_cast(pt.dst_start.day_of_week); + tz.dst_end.time_seconds = pt.dst_end.time_seconds; + tz.dst_end.day = static_cast(pt.dst_end.day); + tz.dst_end.type = static_cast(pt.dst_end.type); + tz.dst_end.month = static_cast(pt.dst_end.month); + tz.dst_end.week = static_cast(pt.dst_end.week); + tz.dst_end.day_of_week = static_cast(pt.dst_end.day_of_week); + time::set_global_tz(tz); + } } #endif } From 28c6fbdc9e29a4e5b9bf7c18f87e359d4531449d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 16:50:33 -0600 Subject: [PATCH 2/3] [api] Mark timezone string field as deprecated in GetTimeResponse parsed_timezone struct should be used instead. The string field will be removed before 2027.1.0. --- esphome/components/api/api.proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 4437617354..f0f7f4a3b1 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -867,7 +867,7 @@ message GetTimeResponse { option (no_delay) = true; fixed32 epoch_seconds = 1; - string timezone = 2; + string timezone = 2 [deprecated = true]; // Use parsed_timezone instead. Remove before 2026.9.0. ParsedTimezone parsed_timezone = 3; } From 0aa43b0c4fcafc628692f6c022ed4314bcdc52c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 16:51:46 -0600 Subject: [PATCH 3/3] tweak --- esphome/components/api/api_pb2.cpp | 4 ---- esphome/components/api/api_pb2.h | 3 +-- esphome/components/api/api_pb2_dump.cpp | 1 - 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 9e74d5ddc7..3f23943b12 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1007,10 +1007,6 @@ bool ParsedTimezone::decode_length(uint32_t field_id, ProtoLengthDelimited value } bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { - this->timezone = StringRef(reinterpret_cast(value.data()), value.size()); - break; - } case 3: value.decode_to_message(this->parsed_timezone); break; diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 2882868982..050b000450 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1154,12 +1154,11 @@ class ParsedTimezone final : public ProtoDecodableMessage { class GetTimeResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 37; - static constexpr uint8_t ESTIMATED_SIZE = 31; + static constexpr uint8_t ESTIMATED_SIZE = 22; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "get_time_response"; } #endif uint32_t epoch_seconds{0}; - StringRef timezone{}; ParsedTimezone parsed_timezone{}; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 290e5fe4d2..869892c691 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1291,7 +1291,6 @@ const char *ParsedTimezone::dump_to(DumpBuffer &out) const { const char *GetTimeResponse::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "GetTimeResponse"); dump_field(out, "epoch_seconds", this->epoch_seconds); - dump_field(out, "timezone", this->timezone); out.append(" parsed_timezone: "); this->parsed_timezone.dump_to(out); out.append("\n");