mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
Harden track_presence generator option and fix test fixtures
Set the presence flag after the submessage decode, dump it in verbose protobuf dumps, and raise at generation time when track_presence is set on a field where it has no effect. Retire the absent-field expectation in the integration test so the UTC phase sees the first matching state, and align the Sydney TZ strings with the 03:00 fall-back the struct helper uses.
This commit is contained in:
@@ -1250,8 +1250,8 @@ bool ParsedTimezone::decode_length(uint32_t field_id, ProtoLengthDelimited value
|
|||||||
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case 3:
|
case 3:
|
||||||
this->has_parsed_timezone = true;
|
|
||||||
value.decode_to_message(this->parsed_timezone);
|
value.decode_to_message(this->parsed_timezone);
|
||||||
|
this->has_parsed_timezone = true;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1468,6 +1468,7 @@ const char *ParsedTimezone::dump_to(DumpBuffer &out) const {
|
|||||||
const char *GetTimeResponse::dump_to(DumpBuffer &out) const {
|
const char *GetTimeResponse::dump_to(DumpBuffer &out) const {
|
||||||
MessageDumpHelper helper(out, ESPHOME_PSTR("GetTimeResponse"));
|
MessageDumpHelper helper(out, ESPHOME_PSTR("GetTimeResponse"));
|
||||||
dump_field(out, ESPHOME_PSTR("epoch_seconds"), this->epoch_seconds);
|
dump_field(out, ESPHOME_PSTR("epoch_seconds"), this->epoch_seconds);
|
||||||
|
dump_field(out, ESPHOME_PSTR("has_parsed_timezone"), this->has_parsed_timezone);
|
||||||
out.append(2, ' ').append_p(ESPHOME_PSTR("parsed_timezone")).append(": ");
|
out.append(2, ' ').append_p(ESPHOME_PSTR("parsed_timezone")).append(": ");
|
||||||
this->parsed_timezone.dump_to(out);
|
this->parsed_timezone.dump_to(out);
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|||||||
@@ -498,6 +498,15 @@ def create_field_type_info(
|
|||||||
needs_encode: bool = True,
|
needs_encode: bool = True,
|
||||||
) -> TypeInfo:
|
) -> TypeInfo:
|
||||||
"""Create the appropriate TypeInfo instance for a field, handling repeated fields and custom options."""
|
"""Create the appropriate TypeInfo instance for a field, handling repeated fields and custom options."""
|
||||||
|
if get_field_opt(field, pb.track_presence, False) and (
|
||||||
|
field.label == FieldDescriptorProto.LABEL_REPEATED
|
||||||
|
or field.type != 11
|
||||||
|
or not needs_decode
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
f"track_presence on field '{field.name}' has no effect; it requires "
|
||||||
|
"a non-repeated message field in a message that is decoded"
|
||||||
|
)
|
||||||
if field.label == FieldDescriptorProto.LABEL_REPEATED:
|
if field.label == FieldDescriptorProto.LABEL_REPEATED:
|
||||||
# Check if this is a packed_buffer field (zero-copy packed repeated)
|
# Check if this is a packed_buffer field (zero-copy packed repeated)
|
||||||
if get_field_opt(field, pb.packed_buffer, False):
|
if get_field_opt(field, pb.packed_buffer, False):
|
||||||
@@ -541,6 +550,8 @@ def create_field_type_info(
|
|||||||
return PointerToStringBufferType(field, None)
|
return PointerToStringBufferType(field, None)
|
||||||
|
|
||||||
validate_field_type(field.type, field.name)
|
validate_field_type(field.type, field.name)
|
||||||
|
if field.type == 11:
|
||||||
|
return MessageType(field, needs_decode, needs_encode)
|
||||||
return TYPE_INFO[field.type](field)
|
return TYPE_INFO[field.type](field)
|
||||||
|
|
||||||
|
|
||||||
@@ -955,10 +966,13 @@ class MessageType(TypeInfo):
|
|||||||
def decode_length_content(self) -> str:
|
def decode_length_content(self) -> str:
|
||||||
# Custom decode that doesn't use templates
|
# Custom decode that doesn't use templates
|
||||||
if self._track_presence:
|
if self._track_presence:
|
||||||
|
# decode_to_message() cannot report failure, so setting the flag
|
||||||
|
# afterwards only documents intent; a status-returning decode could
|
||||||
|
# gate it for real without touching callers.
|
||||||
return (
|
return (
|
||||||
f"case {self.number}:\n"
|
f"case {self.number}:\n"
|
||||||
f" this->has_{self.name} = true;\n"
|
|
||||||
f" value.decode_to_message(this->{self.field_name});\n"
|
f" value.decode_to_message(this->{self.field_name});\n"
|
||||||
|
f" this->has_{self.name} = true;\n"
|
||||||
f" break;"
|
f" break;"
|
||||||
)
|
)
|
||||||
return f"case {self.number}: value.decode_to_message(this->{self.field_name}); break;"
|
return f"case {self.number}: value.decode_to_message(this->{self.field_name}); break;"
|
||||||
@@ -968,7 +982,10 @@ class MessageType(TypeInfo):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def dump_content(self) -> str:
|
def dump_content(self) -> str:
|
||||||
o = f'out.append(2, \' \').append_p(ESPHOME_PSTR("{self.name}")).append(": ");\n'
|
o = ""
|
||||||
|
if self._track_presence:
|
||||||
|
o += f'dump_field(out, ESPHOME_PSTR("has_{self.name}"), this->has_{self.name});\n'
|
||||||
|
o += f'out.append(2, \' \').append_p(ESPHOME_PSTR("{self.name}")).append(": ");\n'
|
||||||
o += f"this->{self.field_name}.dump_to(out);\n"
|
o += f"this->{self.field_name}.dump_to(out);\n"
|
||||||
o += 'out.append("\\n");'
|
o += 'out.append("\\n");'
|
||||||
return o
|
return o
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ static ParsedTimezone make_new_zealand() {
|
|||||||
return tz;
|
return tz;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to build Australia/Sydney timezone (AEST-10AEDT,M10.1.0,M4.1.0)
|
// Helper to build Australia/Sydney timezone (AEST-10AEDT,M10.1.0,M4.1.0/3)
|
||||||
static ParsedTimezone make_australia_sydney() {
|
static ParsedTimezone make_australia_sydney() {
|
||||||
ParsedTimezone tz{};
|
ParsedTimezone tz{};
|
||||||
tz.std_offset_seconds = -10 * 3600;
|
tz.std_offset_seconds = -10 * 3600;
|
||||||
@@ -674,9 +674,9 @@ TEST(RecalcTimestampLocal, FallBackRepeatedHour) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST(RecalcTimestampLocal, SouthernHemisphereDST) {
|
TEST(RecalcTimestampLocal, SouthernHemisphereDST) {
|
||||||
// Set timezone to Australia/Sydney (AEST-10AEDT,M10.1.0,M4.1.0)
|
// Set timezone to Australia/Sydney (AEST-10AEDT,M10.1.0,M4.1.0/3)
|
||||||
// DST starts first Sunday of October, ends first Sunday of April
|
// DST starts first Sunday of October, ends first Sunday of April
|
||||||
const char *tz_str = "AEST-10AEDT,M10.1.0,M4.1.0";
|
const char *tz_str = "AEST-10AEDT,M10.1.0,M4.1.0/3";
|
||||||
setenv("TZ", tz_str, 1);
|
setenv("TZ", tz_str, 1);
|
||||||
tzset();
|
tzset();
|
||||||
auto tz = make_australia_sydney();
|
auto tz = make_australia_sydney();
|
||||||
@@ -817,7 +817,7 @@ TEST(RecalcTimestampLocal, MinimalFieldsNoDST) {
|
|||||||
TEST(RecalcTimestampLocal, YearBoundaryDST) {
|
TEST(RecalcTimestampLocal, YearBoundaryDST) {
|
||||||
// Test southern hemisphere DST across year boundary
|
// Test southern hemisphere DST across year boundary
|
||||||
// Australia/Sydney: DST active from October to April (spans Jan 1)
|
// Australia/Sydney: DST active from October to April (spans Jan 1)
|
||||||
const char *tz_str = "AEST-10AEDT,M10.1.0,M4.1.0";
|
const char *tz_str = "AEST-10AEDT,M10.1.0,M4.1.0/3";
|
||||||
setenv("TZ", tz_str, 1);
|
setenv("TZ", tz_str, 1);
|
||||||
tzset();
|
tzset();
|
||||||
auto tz = make_australia_sydney();
|
auto tz = make_australia_sydney();
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ async def test_api_get_time_response_timezone(
|
|||||||
client._connection.send_messages((resp,))
|
client._connection.send_messages((resp,))
|
||||||
await tracker.await_must_not_change(future, "tz_offset", timeout=1.0)
|
await tracker.await_must_not_change(future, "tz_offset", timeout=1.0)
|
||||||
assert tracker.sensor_states["tz_offset"][-1] == target
|
assert tracker.sensor_states["tz_offset"][-1] == target
|
||||||
|
# Retire the expectation so it cannot swallow the first matching state
|
||||||
|
# meant for the next phase
|
||||||
|
future.cancel()
|
||||||
|
|
||||||
# Present but all zero (genuine UTC): applied
|
# Present but all zero (genuine UTC): applied
|
||||||
future = tracker.expect("tz_offset", 0)
|
future = tracker.expect("tz_offset", 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user