mirror of
https://github.com/esphome/esphome.git
synced 2026-08-26 16:10:29 +00:00
Track parsed_timezone presence and escape host TZ codegen
This commit is contained in:
@@ -1007,7 +1007,7 @@ message GetTimeResponse {
|
||||
// Assistant 2026.3.0 that send only the string leave the device on its
|
||||
// codegen-configured timezone (or UTC).
|
||||
string timezone = 2 [deprecated = true];
|
||||
ParsedTimezone parsed_timezone = 3;
|
||||
ParsedTimezone parsed_timezone = 3 [(track_presence) = true];
|
||||
}
|
||||
|
||||
// ==================== USER-DEFINES SERVICES ====================
|
||||
|
||||
@@ -1205,12 +1205,11 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
|
||||
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
|
||||
#if defined(USE_HOMEASSISTANT_TIMEZONE) && defined(USE_TIME_TIMEZONE)
|
||||
// Apply only if the sender provided pre-parsed timezone data (Home Assistant 2026.3.0
|
||||
// and newer). Older clients send only the deprecated timezone string, which is no
|
||||
// longer decoded; for them the struct stays all-zero and the device keeps its
|
||||
// codegen-configured timezone. Actual UTC (all zeros) is also skipped, which is
|
||||
// harmless since UTC is the default.
|
||||
const auto &pt = value.parsed_timezone;
|
||||
if (pt.std_offset_seconds != 0 || pt.dst_start.type != enums::DST_RULE_TYPE_NONE) {
|
||||
// and newer); field presence distinguishes a genuine all-zero UTC timezone from an
|
||||
// absent field. Older clients send only the deprecated timezone string, which is no
|
||||
// longer decoded; for them the device keeps its codegen-configured timezone.
|
||||
if (value.has_parsed_timezone) {
|
||||
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;
|
||||
|
||||
@@ -116,4 +116,10 @@ extend google.protobuf.FieldOptions {
|
||||
// the per-byte loop when the upper bits are non-zero (the common case
|
||||
// for real MAC addresses, since OUIs occupy the top 24 bits).
|
||||
optional bool mac_address = 50019 [default=false];
|
||||
|
||||
// track_presence: Track whether this message-typed field was present on the wire.
|
||||
// Generates a `bool has_<field>{false};` member on the decoding side that is set
|
||||
// to true when the field arrives, so an all-default submessage can be told apart
|
||||
// from an absent one (e.g. a UTC ParsedTimezone, which is all zeros).
|
||||
optional bool track_presence = 50020 [default=false];
|
||||
}
|
||||
|
||||
@@ -1250,6 +1250,7 @@ bool ParsedTimezone::decode_length(uint32_t field_id, ProtoLengthDelimited value
|
||||
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 3:
|
||||
this->has_parsed_timezone = true;
|
||||
value.decode_to_message(this->parsed_timezone);
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -1289,6 +1289,7 @@ class GetTimeResponse final : public ProtoDecodableMessage {
|
||||
#endif
|
||||
uint32_t epoch_seconds{0};
|
||||
ParsedTimezone parsed_timezone{};
|
||||
bool has_parsed_timezone{false};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
@@ -36,6 +36,7 @@ from esphome.const import (
|
||||
PLATFORM_RTL87XX,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority
|
||||
from esphome.helpers import cpp_string_escape
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -412,7 +413,7 @@ async def setup_time_core_(time_var, config):
|
||||
|
||||
if CORE.is_host:
|
||||
# Host platform also needs setenv("TZ")/tzset() for libc compatibility
|
||||
cg.add(cg.RawExpression(f'setenv("TZ", "{timezone}", 1)'))
|
||||
cg.add(cg.RawExpression(f'setenv("TZ", {cpp_string_escape(timezone)}, 1)'))
|
||||
cg.add(cg.RawExpression("tzset()"))
|
||||
|
||||
# Pre-parse at codegen time, emit struct directly
|
||||
|
||||
@@ -937,9 +937,29 @@ class MessageType(TypeInfo):
|
||||
# runtime polymorphism through virtual function calls.
|
||||
return None
|
||||
|
||||
@property
|
||||
def public_content(self) -> list[str]:
|
||||
content = [self.class_member]
|
||||
if self._track_presence:
|
||||
content.append(f"bool has_{self.name}{{false}};")
|
||||
return content
|
||||
|
||||
@property
|
||||
def _track_presence(self) -> bool:
|
||||
# Use getattr to handle older versions of api_options_pb2
|
||||
opt = getattr(pb, "track_presence", None)
|
||||
return opt is not None and get_field_opt(self._field, opt, False)
|
||||
|
||||
@property
|
||||
def decode_length_content(self) -> str:
|
||||
# Custom decode that doesn't use templates
|
||||
if self._track_presence:
|
||||
return (
|
||||
f"case {self.number}:\n"
|
||||
f" this->has_{self.name} = true;\n"
|
||||
f" value.decode_to_message(this->{self.field_name});\n"
|
||||
f" break;"
|
||||
)
|
||||
return f"case {self.number}: value.decode_to_message(this->{self.field_name}); break;"
|
||||
|
||||
def dump(self, name: str) -> str:
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
network:
|
||||
|
||||
api:
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
# Angle-bracket name pins the explicit-timezone host codegen path
|
||||
# (setenv/tzset plus pre-parsed struct emission) with characters that
|
||||
# would break unescaped string interpolation.
|
||||
timezone: "<+07>-7"
|
||||
Reference in New Issue
Block a user