Compare commits

...
Author SHA1 Message Date
J. Nick Koston 5ac2fa8811 [api] Print the dropped action's service with its length
The field can now start as a null StringRef, so the log passes the size and an empty
literal instead of a pointer that may be null.
2026-09-08 04:42:58 +02:00
J. Nick Koston e715696bd7 [core] Bound StringRef's JSON conversion by the view length 2026-09-08 04:15:24 +02:00
J. Nick Koston 82d399400e [core] Convert a null StringRef to an empty JSON string and pin null against null 2026-09-08 03:44:24 +02:00
J. Nick Koston a7bf937001 [core] Keep StringRef::str() inline
The std::string range constructor reads nothing for a zero length, so the guard only
pushed str() out of line; the memcmp guards stay.
2026-09-08 03:22:05 +02:00
J. Nick Koston 63b5331e47 [core] Let StringRef carry a null pointer at zero length
The generated api messages start their encode only string fields that way. starts_with,
compare and str() no longer hand a null pointer to memcmp or the std::string range
constructor when there is nothing to compare or copy, the class comment states the
contract, and gtest cases pin every member on a null, empty view.
2026-09-08 03:14:44 +02:00
J. Nick Koston 4c72948575 [api] Say why the pointer buffer base keeps its constructor 2026-09-08 00:24:14 +02:00
J. Nick Koston 77d850cbe2 [api] Drop the unused array_size on the pointer buffer base 2026-09-08 00:07:54 +02:00
J. Nick Koston a97e4ebc1c [api] Pass needs_decode at both bytes buffer sites and guard the second unconditional copy path 2026-09-07 23:46:15 +02:00
J. Nick Koston d09f642eb2 [api] Name the null default invariant once and thread needs_decode through the pointer buffer base
The dead size parameter goes, the string type reads the inherited flag, the forced short
string path asserts against it, and the generated declarations say why the pointer may be
null.
2026-09-07 23:23:19 +02:00
J. Nick Koston 144cd419ad [api] Test which string fields get the null default 2026-09-07 23:11:47 +02:00
J. Nick Koston 7910c372cd [api] Default response only string fields to a null StringRef
A StringRef field that is only ever encoded is skipped when empty before its pointer is
read, and the dump helper checks empty() first, so pointing it at "" buys nothing while
costing one store per field in every message constructor. Fields that are decoded or force
encoded keep the empty string default.
2026-09-07 23:04:33 +02:00
J. Nick Koston c2a4981e25 [api] Order the wrong wire type assertions on the raw socket
The barrier was a command on the aioesphomeapi connection, which nothing orders against the
raw frames under test; a well formed frame on the raw socket itself now closes each block and
the assertion checks the exact states seen since the marker.
2026-09-07 16:22:42 +02:00
J. Nick Koston 097d487261 [api] Cover truncated bodies in the decode integration test and pin the 64 bit field rejection
Three malformed frames (a tag with a dangling continuation bit, a length prefix past the
payload, a two byte fixed32) must stop the decode loop without taking the connection down;
a generator test records that a double field is rejected before it could reach the loop.
2026-09-07 15:58:28 +02:00
J. Nick Koston 89082c7b2d [api] Cover repeated and sub message fields in the decode case tests 2026-09-07 15:57:14 +02:00
J. Nick Koston fb3befe377 [api] Second cleanup pass over the decode generator
Repeated fields encode their elements through one encode_element() hook instead of two
isinstance ladders, the fixed32 precomputed tag path owns its own guard, the generated
switches drop the dead default case, StringRef takes the byte pointer directly, the
three hand written tag expressions in proto.h go through proto_tag(), and stale comments
about the previous decode design go. The compiled functions are unchanged.
2026-09-07 15:57:14 +02:00
J. Nick Koston 4a3a787756 [api] Return the varint parse result as a struct again
The out parameter form regressed the host, where the 16 byte result already travels in
registers, by about 20 percent on the direct varint parse benchmarks. The void
decode_field and low word bool changes stay.
2026-09-07 15:57:13 +02:00
J. Nick Koston 5aec93d7e8 [api] Trim the decode path: void decode_field, bool from the low word, slow varint out parameter
decode_field() no longer returns a bool that only fed a verbose log; unknown fields are
skipped silently like every other protobuf decoder does, and each message loses the
return value materialisation. Bools read the low 32 bits of the varint, which drops the
second compare on 64 bit varint builds. The multi byte varint path writes its value
through an out parameter instead of returning a 16 byte struct, which takes the spills
out of the decode loop and count_repeated_field.
2026-09-07 15:57:13 +02:00
J. Nick Koston a79cd1550c [api] Collapse the generator's per wire type decode hooks into one
A type now sets a single decode_expr; the wire type it already declares picks the case
label. Drops the unused force_str helper, two dead decode_length overrides, a duplicate
field builder in the generator tests and a needless list copy in the state waiter. The
generated files are unchanged.
2026-09-07 15:57:13 +02:00
J. Nick Koston 6d23e4c842 [api] Key the generated decode switch on the wire tag on every target
The host no longer gets its own switch shape through USE_HOST; every build compiles the
same switch on the field's wire tag.
2026-09-07 15:57:13 +02:00
J. Nick Koston cbdcfe640c [api] Read fixed32 fields with byte loads and test the varint wire type first
ESP-IDF passes -fno-builtin-memcpy, so the four byte memcpy in the decode loop was an
out of line call on every fixed32 field; host compilers fold the byte loads back into one
load. Checking the varint wire type first keeps the common path to one taken branch on
xtensa.
2026-09-07 15:57:13 +02:00
J. Nick Koston 2bc20d8721 [api] Derive decode cases from the type's wire type
decode_case() reads wire_type instead of taking it at every call, the
expression and the store statement are two small hooks that repeated
fields override, and message fields build one body. No generated case
declares a local any more, so the braced form and its test go; the
compiler rejects a jump over a local if one ever appears. The wire
type test now proves a dropped frame with an ordering marker instead
of assuming it, and shares StateWaiter.
2026-09-07 15:57:13 +02:00
J. Nick Koston 2dbb3e055b [api] Add an integration test for decode wire type handling
Hand built frames check that decode_field() takes a field with its
declared wire type, drops the same field sent length delimited or as
fixed32, ignores a varint key, and skips an unknown field with a two
byte tag before decoding the rest. Client commands cover two byte tags
and varints, a two byte length prefix and a negative fixed32 float.
2026-09-07 15:57:13 +02:00
J. Nick Koston 7549309ad3 [api] Trim the decode dispatch comments 2026-09-07 15:57:13 +02:00
J. Nick Koston e600180417 [api] Emit one decode case per field from a single generator property
With one decode_field() switch per message, the three per wire type
content properties only differed in the attribute they read; a single
decode_content built from decode_expr() replaces them, and repeated
fields reuse the element type's expression. Case bodies with several
statements get their block from the body itself instead of a caller
flag, the fixed byte array body copies straight from the payload
instead of through a heap std::string, and the decode comments no
longer restate the switch keying explained next to the macros.
2026-09-07 15:57:13 +02:00
J. Nick Koston 0641dae9d2 [api] Scope generated decode cases that declare locals
A case body with a declaration or several statements now gets its own
block, as the per wire type overrides had, so no jump to a later case
label crosses an initialization.
2026-09-07 15:57:13 +02:00
J. Nick Koston bcf812d62b [api] Keep the decode loop register resident and inline the varint fast path
CodSpeed showed the single virtual costing 7 to 18 percent on the
decode benchmarks. The x86-64 disassembly pointed at the call, not the
switch: passing the field number and wire type alongside the tag plus
a 16 byte union payload kept five values live across the call, so the
compiler spilled this, the end pointer and half of the payload to the
stack and reloaded them for every field.

decode_field() now takes only the tag, the payload pointer (already
the loop cursor) and one scalar that holds the varint or fixed32 value
or the payload length. The generated override wraps them in a
ProtoFieldValue that never exists in memory. On the host the switch
key is the field number derived with one shift and the guard compares
the whole tag against the constant the case declares, which is the
same two instructions the old per wire type dispatch cost.

The loop also handles single byte varints inline instead of going
through the parse result struct, which drops the materialized consumed
count and its add on every tag and small value.
2026-09-07 15:57:13 +02:00
J. Nick Koston b0ce7f58f3 [api] Collapse the three protobuf decode virtuals into one
Every decodable message overrode up to three virtuals, one per wire
type, so each carried a five slot vtable and up to three functions
with their own prologue and return tails. The shared decode loop now
parses the payload for the wire type into a ProtoFieldValue and calls
a single decode_field() virtual with the tag, the field number and the
wire type; the generated override is one switch.

The switch key is chosen per target through PROTO_DECODE_KEY. Embedded
builds compile switches to compare chains (ESP-IDF passes
-fno-jump-tables), so they key on the full wire tag, one compare per
field with no separate wire type check. The host compiler builds a
jump table for the dense field number switch, so there the key is the
field number and PROTO_DECODE_GUARD rejects a mismatched wire type.
Both forms drop a field that arrives with a wire type it does not
declare, exactly as the per wire type virtuals did.

Per decodable message the vtable shrinks from 20 to 12 bytes on
xtensa and the extra decode functions fold into one; the shared loop
shrinks as well. Host instruction counts per decoded field are
unchanged apart from the guard compare, which replaces the prologue of
the separate function it used to call.
2026-09-07 15:57:12 +02:00
13 changed files with 1494 additions and 1784 deletions
File diff suppressed because it is too large Load Diff
+106 -153
View File
@@ -377,7 +377,7 @@ class InfoResponseProtoMessage : public ProtoMessage {
StringRef name{};
bool disabled_by_default{false};
#ifdef USE_ENTITY_ICON
StringRef icon{};
StringRef icon{nullptr, 0}; // null until set, encode only
#endif
enums::EntityCategory entity_category{};
#ifdef USE_DEVICES
@@ -424,8 +424,7 @@ class HelloRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class HelloResponse final : public ProtoMessage {
public:
@@ -469,7 +468,7 @@ class DisconnectRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class DisconnectResponse final : public ProtoMessage {
public:
@@ -550,7 +549,7 @@ class DeviceInfo final : public ProtoMessage {
#ifdef USE_SERIAL_PROXY
class SerialProxyInfo final : public ProtoMessage {
public:
StringRef name{};
StringRef name{nullptr, 0}; // null until set, encode only
enums::SerialProxyPortType port_type{};
uint32_t configured_line_states{0};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
@@ -745,7 +744,7 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_binary_sensor_response"); }
#endif
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
bool is_status_binary_sensor{false};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -792,7 +791,7 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage {
bool assumed_state{false};
bool supports_position{false};
bool supports_tilt{false};
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
bool supports_stop{false};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -845,8 +844,7 @@ class CoverCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_FAN
@@ -885,7 +883,7 @@ class FanStateResponse final : public StateResponseProtoMessage {
bool oscillating{false};
enums::FanDirection direction{};
int32_t speed_level{0};
StringRef preset_mode{};
StringRef preset_mode{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -920,9 +918,7 @@ class FanCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_LIGHT
@@ -967,7 +963,7 @@ class LightStateResponse final : public StateResponseProtoMessage {
float color_temperature{0.0f};
float cold_white{0.0f};
float warm_white{0.0f};
StringRef effect{};
StringRef effect{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -1018,9 +1014,7 @@ class LightCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_SENSOR
@@ -1031,10 +1025,10 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_sensor_response"); }
#endif
StringRef unit_of_measurement{};
StringRef unit_of_measurement{nullptr, 0}; // null until set, encode only
int32_t accuracy_decimals{0};
bool force_update{false};
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
enums::SensorStateClass state_class{};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1079,7 +1073,7 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage {
const LogString *message_name() const override { return LOG_STR("list_entities_switch_response"); }
#endif
bool assumed_state{false};
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -1125,8 +1119,7 @@ class SwitchCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_TEXT_SENSOR
@@ -1137,7 +1130,7 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_text_sensor_response"); }
#endif
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -1157,7 +1150,7 @@ class TextSensorStateResponse final : public StateResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("text_sensor_state_response"); }
#endif
StringRef state{};
StringRef state{nullptr, 0}; // null until set, encode only
bool missing_state{false};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1186,7 +1179,7 @@ class SubscribeLogsRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class SubscribeLogsResponse final : public ProtoMessage {
public:
@@ -1229,7 +1222,7 @@ class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class NoiseEncryptionSetKeyResponse final : public ProtoMessage {
public:
@@ -1255,8 +1248,8 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage {
#ifdef USE_API_HOMEASSISTANT_SERVICES
class HomeassistantServiceMap final : public ProtoMessage {
public:
StringRef key{};
StringRef value{};
StringRef key{nullptr, 0}; // null until set, encode only
StringRef value{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -1276,7 +1269,7 @@ class HomeassistantActionRequest final : public ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("homeassistant_action_request"); }
#endif
StringRef service{};
StringRef service{nullptr, 0}; // null until set, encode only
FixedVector<HomeassistantServiceMap> data{};
FixedVector<HomeassistantServiceMap> data_template{};
FixedVector<HomeassistantServiceMap> variables{};
@@ -1288,7 +1281,7 @@ class HomeassistantActionRequest final : public ProtoMessage {
bool wants_response{false};
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
StringRef response_template{};
StringRef response_template{nullptr, 0}; // null until set, encode only
#endif
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1323,8 +1316,7 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage {
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
@@ -1335,8 +1327,8 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("subscribe_home_assistant_state_response"); }
#endif
StringRef entity_id{};
StringRef attribute{};
StringRef entity_id{nullptr, 0}; // null until set, encode only
StringRef attribute{nullptr, 0}; // null until set, encode only
bool once{false};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1365,7 +1357,7 @@ class HomeAssistantStateResponse final : public ProtoDecodableMessage {
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
class GetTimeRequest final : public ProtoMessage {
@@ -1394,7 +1386,7 @@ class DSTRule final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class ParsedTimezone final : public ProtoDecodableMessage {
public:
@@ -1407,8 +1399,7 @@ class ParsedTimezone final : public ProtoDecodableMessage {
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class GetTimeResponse final : public ProtoDecodableMessage {
public:
@@ -1425,19 +1416,18 @@ class GetTimeResponse final : public ProtoDecodableMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#ifdef USE_API_USER_DEFINED_ACTIONS
class ListEntitiesServicesArgument final : public ProtoMessage {
public:
StringRef name{};
StringRef name{nullptr, 0}; // null until set, encode only
enums::ServiceArgType type{};
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef description{};
StringRef description{nullptr, 0}; // null until set, encode only
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef example{};
StringRef example{nullptr, 0}; // null until set, encode only
#endif
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1458,12 +1448,12 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_services_response"); }
#endif
StringRef name{};
StringRef name{nullptr, 0}; // null until set, encode only
uint32_t key{0};
FixedVector<ListEntitiesServicesArgument> args{};
enums::SupportsResponseType supports_response{};
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef description{};
StringRef description{nullptr, 0}; // null until set, encode only
#endif
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1494,9 +1484,7 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class ExecuteServiceRequest final : public ProtoDecodableMessage {
public:
@@ -1519,9 +1507,7 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
@@ -1534,7 +1520,7 @@ class ExecuteServiceResponse final : public ProtoMessage {
#endif
uint32_t call_id{0};
bool success{false};
StringRef error_message{};
StringRef error_message{nullptr, 0}; // null until set, encode only
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
const uint8_t *response_data{nullptr};
uint16_t response_data_len{0};
@@ -1612,7 +1598,7 @@ class CameraImageRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_CLIMATE
@@ -1669,9 +1655,9 @@ class ClimateStateResponse final : public StateResponseProtoMessage {
enums::ClimateAction action{};
enums::ClimateFanMode fan_mode{};
enums::ClimateSwingMode swing_mode{};
StringRef custom_fan_mode{};
StringRef custom_fan_mode{nullptr, 0}; // null until set, encode only
enums::ClimatePreset preset{};
StringRef custom_preset{};
StringRef custom_preset{nullptr, 0}; // null until set, encode only
float current_humidity{0.0f};
float target_humidity{0.0f};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
@@ -1718,9 +1704,7 @@ class ClimateCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_WATER_HEATER
@@ -1792,8 +1776,7 @@ class WaterHeaterCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_NUMBER
@@ -1807,9 +1790,9 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage {
float min_value{0.0f};
float max_value{0.0f};
float step{0.0f};
StringRef unit_of_measurement{};
StringRef unit_of_measurement{nullptr, 0}; // null until set, encode only
enums::NumberMode mode{};
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -1856,8 +1839,7 @@ class NumberCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_SELECT
@@ -1888,7 +1870,7 @@ class SelectStateResponse final : public StateResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("select_state_response"); }
#endif
StringRef state{};
StringRef state{nullptr, 0}; // null until set, encode only
bool missing_state{false};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1915,9 +1897,7 @@ class SelectCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_SIREN
@@ -1983,9 +1963,7 @@ class SirenCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_LOCK
@@ -1999,7 +1977,7 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage {
bool assumed_state{false};
bool supports_open{false};
bool requires_code{false};
StringRef code_format{};
StringRef code_format{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -2047,9 +2025,7 @@ class LockCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_BUTTON
@@ -2060,7 +2036,7 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_button_response"); }
#endif
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -2085,14 +2061,13 @@ class ButtonCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_MEDIA_PLAYER
class MediaPlayerSupportedFormat final : public ProtoMessage {
public:
StringRef format{};
StringRef format{nullptr, 0}; // null until set, encode only
uint32_t sample_rate{0};
uint32_t num_channels{0};
enums::MediaPlayerFormatPurpose purpose{};
@@ -2172,9 +2147,7 @@ class MediaPlayerCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_BLUETOOTH_PROXY
@@ -2191,7 +2164,7 @@ class SubscribeBluetoothLEAdvertisementsRequest final : public ProtoDecodableMes
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class BluetoothLERawAdvertisement final : public ProtoMessage {
public:
@@ -2245,7 +2218,7 @@ class BluetoothDeviceRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class BluetoothDeviceConnectionResponse final : public ProtoMessage {
public:
@@ -2283,7 +2256,7 @@ class BluetoothGATTGetServicesRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class BluetoothGATTDescriptor final : public ProtoMessage {
public:
@@ -2394,7 +2367,7 @@ class BluetoothGATTReadRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class BluetoothGATTReadResponse final : public ProtoMessage {
public:
@@ -2440,8 +2413,7 @@ class BluetoothGATTWriteRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage {
public:
@@ -2457,7 +2429,7 @@ class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage {
public:
@@ -2475,8 +2447,7 @@ class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class BluetoothGATTNotifyRequest final : public ProtoDecodableMessage {
public:
@@ -2493,7 +2464,7 @@ class BluetoothGATTNotifyRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class BluetoothGATTNotifyDataResponse final : public ProtoMessage {
public:
@@ -2711,7 +2682,7 @@ class BluetoothScannerSetModeRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_VOICE_ASSISTANT
@@ -2729,7 +2700,7 @@ class SubscribeVoiceAssistantRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class VoiceAssistantAudioSettings final : public ProtoMessage {
public:
@@ -2756,10 +2727,10 @@ class VoiceAssistantRequest final : public ProtoMessage {
const LogString *message_name() const override { return LOG_STR("voice_assistant_request"); }
#endif
bool start{false};
StringRef conversation_id{};
StringRef conversation_id{nullptr, 0}; // null until set, encode only
uint32_t flags{0};
VoiceAssistantAudioSettings audio_settings{};
StringRef wake_word_phrase{};
StringRef wake_word_phrase{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -2786,7 +2757,7 @@ class VoiceAssistantResponse final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class VoiceAssistantEventData final : public ProtoDecodableMessage {
public:
@@ -2797,7 +2768,7 @@ class VoiceAssistantEventData final : public ProtoDecodableMessage {
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class VoiceAssistantEventResponse final : public ProtoDecodableMessage {
public:
@@ -2813,8 +2784,7 @@ class VoiceAssistantEventResponse final : public ProtoDecodableMessage {
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class VoiceAssistantAudio final : public ProtoDecodableMessage {
public:
@@ -2839,8 +2809,7 @@ class VoiceAssistantAudio final : public ProtoDecodableMessage {
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage {
public:
@@ -2860,8 +2829,7 @@ class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage {
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage {
public:
@@ -2879,8 +2847,7 @@ class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class VoiceAssistantAnnounceFinished final : public ProtoMessage {
public:
@@ -2904,8 +2871,8 @@ class VoiceAssistantAnnounceFinished final : public ProtoMessage {
};
class VoiceAssistantWakeWord final : public ProtoMessage {
public:
StringRef id{};
StringRef wake_word{};
StringRef id{nullptr, 0}; // null until set, encode only
StringRef wake_word{nullptr, 0}; // null until set, encode only
std::vector<std::string> trained_languages{};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -2933,8 +2900,7 @@ class VoiceAssistantExternalWakeWord final : public ProtoDecodableMessage {
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class VoiceAssistantConfigurationRequest final : public ProtoDecodableMessage {
public:
@@ -2949,7 +2915,7 @@ class VoiceAssistantConfigurationRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class VoiceAssistantConfigurationResponse final : public ProtoMessage {
public:
@@ -2986,7 +2952,7 @@ class VoiceAssistantSetConfiguration final : public ProtoDecodableMessage {
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_ALARM_CONTROL_PANEL
@@ -3046,9 +3012,7 @@ class AlarmControlPanelCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_TEXT
@@ -3061,7 +3025,7 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage {
#endif
uint32_t min_length{0};
uint32_t max_length{0};
StringRef pattern{};
StringRef pattern{nullptr, 0}; // null until set, encode only
enums::TextMode mode{};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -3082,7 +3046,7 @@ class TextStateResponse final : public StateResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("text_state_response"); }
#endif
StringRef state{};
StringRef state{nullptr, 0}; // null until set, encode only
bool missing_state{false};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -3109,9 +3073,7 @@ class TextCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_DATETIME_DATE
@@ -3172,8 +3134,7 @@ class DateCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_DATETIME_TIME
@@ -3234,8 +3195,7 @@ class TimeCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_EVENT
@@ -3246,7 +3206,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_event_response"); }
#endif
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
const FixedVector<const char *> *event_types{};
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -3267,7 +3227,7 @@ class EventResponse final : public StateResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("event_response"); }
#endif
StringRef event_type{};
StringRef event_type{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -3289,7 +3249,7 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_valve_response"); }
#endif
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
bool assumed_state{false};
bool supports_position{false};
bool supports_stop{false};
@@ -3341,8 +3301,7 @@ class ValveCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_DATETIME_DATETIME
@@ -3399,8 +3358,7 @@ class DateTimeCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_UPDATE
@@ -3411,7 +3369,7 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_update_response"); }
#endif
StringRef device_class{};
StringRef device_class{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -3435,11 +3393,11 @@ class UpdateStateResponse final : public StateResponseProtoMessage {
bool in_progress{false};
bool has_progress{false};
float progress{0.0f};
StringRef current_version{};
StringRef latest_version{};
StringRef title{};
StringRef release_summary{};
StringRef release_url{};
StringRef current_version{nullptr, 0}; // null until set, encode only
StringRef latest_version{nullptr, 0}; // null until set, encode only
StringRef title{nullptr, 0}; // null until set, encode only
StringRef release_summary{nullptr, 0}; // null until set, encode only
StringRef release_url{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -3465,8 +3423,7 @@ class UpdateCommandRequest final : public CommandProtoMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
#endif
#ifdef USE_ZWAVE_PROXY
@@ -3490,7 +3447,7 @@ class ZWaveProxyFrame final : public ProtoDecodableMessage {
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class ZWaveProxyRequest final : public ProtoDecodableMessage {
public:
@@ -3513,8 +3470,7 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class ZWaveProxyRequestResponse final : public ProtoMessage {
public:
@@ -3584,9 +3540,7 @@ class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class InfraredRFReceiveEvent final : public ProtoMessage {
public:
@@ -3657,7 +3611,7 @@ class SerialProxyConfigureRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class SerialProxyDataReceived final : public ProtoMessage {
public:
@@ -3700,8 +3654,7 @@ class SerialProxyWriteRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class SerialProxySetModemPinsRequest final : public ProtoDecodableMessage {
public:
@@ -3717,7 +3670,7 @@ class SerialProxySetModemPinsRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage {
public:
@@ -3732,7 +3685,7 @@ class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class SerialProxyGetModemPinsResponse final : public ProtoMessage {
public:
@@ -3770,7 +3723,7 @@ class SerialProxyRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class SerialProxyRequestResponse final : public ProtoMessage {
public:
@@ -3782,7 +3735,7 @@ class SerialProxyRequestResponse final : public ProtoMessage {
uint32_t instance{0};
enums::SerialProxyRequestType type{};
enums::SerialProxyStatus status{};
StringRef error_message{};
StringRef error_message{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
@@ -3814,7 +3767,7 @@ class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;
};
class BluetoothSetConnectionParamsResponse final : public ProtoMessage {
public:
+3 -2
View File
@@ -433,8 +433,9 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
// Home Assistant subscribes to actions shortly *after* authenticating, so actions
// fired right at connection time (on_client_connected, on_time_sync, ...) can
// arrive before the subscription and are lost - warn instead of failing silently.
ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s",
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"), call.service.c_str(),
ESP_LOGW(TAG, "Home Assistant %s '%.*s' dropped; %s",
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"),
static_cast<int>(call.service.size()), call.service.empty() ? "" : call.service.c_str(),
this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)")
: LOG_STR_LITERAL("no client connected"));
}
+59 -58
View File
@@ -214,73 +214,74 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
const uint8_t *ptr = buffer;
const uint8_t *end = buffer + length;
while (ptr < end) {
// Parse field header - ptr < end guarantees len >= 1
// Single-byte varints dominate, so that case advances the cursor inline.
auto read_varint = [&](proto_varint_value_t &value) ESPHOME_ALWAYS_INLINE {
if (ptr == end)
return false;
if (*ptr < 0x80) [[likely]] {
value = *ptr++;
return true;
}
auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
if (!res.has_value()) {
if (!res.has_value())
return false;
value = res.value;
ptr += res.consumed;
return true;
};
while (ptr < end) {
proto_varint_value_t tag_value;
if (!read_varint(tag_value)) {
ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer));
return;
}
uint32_t tag = static_cast<uint32_t>(res.value);
uint32_t tag = static_cast<uint32_t>(tag_value);
uint32_t field_type = tag & WIRE_TYPE_MASK;
uint32_t field_id = tag >> 3;
ptr += res.consumed;
// Length-delimited fields move this past the length prefix
const uint8_t *data = ptr;
proto_varint_value_t scalar;
switch (field_type) {
case WIRE_TYPE_VARINT: { // VarInt
res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
return;
}
if (!this->decode_varint(field_id, res.value)) {
ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu64 "!", field_id,
static_cast<uint64_t>(res.value));
}
ptr += res.consumed;
break;
}
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited
res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
uint32_t field_length = static_cast<uint32_t>(res.value);
ptr += res.consumed;
if (field_length > static_cast<size_t>(end - ptr)) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
if (!this->decode_length(field_id, ProtoLengthDelimited(ptr, field_length))) {
ESP_LOGV(TAG, "Cannot decode Length Delimited field %" PRIu32 "!", field_id);
}
ptr += field_length;
break;
}
case WIRE_TYPE_FIXED32: { // 32-bit
if (end - ptr < 4) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return;
}
uint32_t val;
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
// Protobuf fixed32 is little-endian — direct load on LE platforms
memcpy(&val, ptr, 4);
#else
val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
#endif
if (!this->decode_32bit(field_id, Proto32Bit(val))) {
ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val);
}
ptr += 4;
break;
}
default:
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
if (field_type == WIRE_TYPE_VARINT) [[likely]] {
if (!read_varint(scalar)) {
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
return;
}
} else {
switch (field_type) {
case WIRE_TYPE_LENGTH_DELIMITED: {
proto_varint_value_t length_value;
if (!read_varint(length_value)) {
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
uint32_t field_length = static_cast<uint32_t>(length_value);
if (field_length > static_cast<size_t>(end - ptr)) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
data = ptr;
scalar = field_length;
ptr += field_length;
break;
}
case WIRE_TYPE_FIXED32: {
if (end - ptr < 4) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return;
}
// Byte loads instead of memcpy: ESP-IDF passes -fno-builtin-memcpy, which made this a call
scalar = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
ptr += 4;
break;
}
default:
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
return;
}
}
this->decode_field(tag, data, scalar);
}
}
+31 -33
View File
@@ -170,40 +170,43 @@ class ProtoVarInt {
class ProtoMessage;
class ProtoSize;
class ProtoLengthDelimited {
/// Case label for decode_field(): the wire tag of a field, so a field that arrives with another wire
/// type matches no case.
constexpr uint32_t proto_tag(uint32_t field_id, uint32_t wire_type) { return (field_id << 3) | wire_type; }
/// One decoded field: the payload pointer and a scalar holding the varint or fixed32 value, or the
/// length of a length-delimited field. The wire type in the tag says which applies; accessors do not check.
class ProtoFieldValue {
public:
explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {}
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); }
ProtoFieldValue(const uint8_t *data, proto_varint_value_t scalar) : data_(data), scalar_(scalar) {}
// Direct access to raw data without string allocation
const uint8_t *data() const { return this->value_; }
size_t size() const { return this->length_; }
proto_varint_value_t as_varint() const { return this->scalar_; }
// A bool is sent as 0 or 1, so the low word is enough and saves a second compare with 64 bit varints
bool as_bool() const { return static_cast<uint32_t>(this->scalar_) != 0; }
/// Decode the length-delimited data into a message instance.
// Length-delimited accessors
const uint8_t *data() const { return this->data_; }
size_t size() const { return static_cast<size_t>(this->scalar_); }
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->data_), this->size()); }
/// Decode the length-delimited payload into a message instance.
/// Template preserves concrete type so decode() resolves statically.
template<typename T> void decode_to_message(T &msg) const;
template<typename T> void decode_to_message(T &msg) const { msg.decode(this->data_, this->size()); }
protected:
const uint8_t *const value_;
const size_t length_;
};
class Proto32Bit {
public:
explicit Proto32Bit(uint32_t value) : value_(value) {}
uint32_t as_fixed32() const { return this->value_; }
int32_t as_sfixed32() const { return static_cast<int32_t>(this->value_); }
// Fixed32 accessors
uint32_t as_fixed32() const { return static_cast<uint32_t>(this->scalar_); }
int32_t as_sfixed32() const { return static_cast<int32_t>(this->as_fixed32()); }
float as_float() const {
union {
uint32_t raw;
float value;
} s{};
s.raw = this->value_;
s.raw = this->as_fixed32();
return s.value;
}
protected:
const uint32_t value_;
private:
const uint8_t *data_;
proto_varint_value_t scalar_;
};
// NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported
@@ -252,7 +255,7 @@ class ProtoWriteBuffer {
*
* Following https://protobuf.dev/programming-guides/encoding/#structure
*/
void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); }
void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw(proto_tag(field_id, type)); }
/// Single-pass encode for repeated submessage elements.
/// Thin template wrapper; all buffer work is in the non-template core.
template<typename T> void encode_sub_message(uint32_t field_id, const T &value);
@@ -382,7 +385,7 @@ class ProtoEncode {
}
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
encode_field_raw(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, uint32_t type) {
return encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, (field_id << 3) | type);
return encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, proto_tag(field_id, type));
}
/// Write a single precomputed tag byte. Tag must be < 128.
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
@@ -732,10 +735,10 @@ class ProtoDecodableMessage : public ProtoMessage {
protected:
~ProtoDecodableMessage() = default;
virtual bool decode_varint(uint32_t field_id, proto_varint_value_t value) { return false; }
virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; }
virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; }
// NOTE: decode_64bit removed - wire type 1 not supported
/// Store one decoded field; \p scalar is the varint or fixed32 value, or the length of the
/// length-delimited payload at \p data. An unknown field or wrong wire type matches no case and is skipped.
/// Three register arguments keep the decode loop free of spills.
virtual void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {}
};
class ProtoSize {
@@ -861,7 +864,7 @@ class ProtoSize {
* @return The number of bytes needed to encode the field ID and wire type
*/
static constexpr uint32_t field(uint32_t field_id, uint32_t type) {
uint32_t tag = (field_id << 3) | (type & WIRE_TYPE_MASK);
uint32_t tag = proto_tag(field_id, type & WIRE_TYPE_MASK);
return varint(tag);
}
@@ -955,11 +958,6 @@ template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(u
this->encode_optional_sub_message(field_id, T::calc_size_msg(&value), &value, &T::encode_msg);
}
// Template decode_to_message - preserves concrete type so decode() resolves statically
template<typename T> void ProtoLengthDelimited::decode_to_message(T &msg) const {
msg.decode(this->value_, this->length_);
}
template<typename T> const char *proto_enum_to_string(T value);
// ProtoService removed — its methods were inlined into APIConnection.
+16 -4
View File
@@ -22,6 +22,10 @@ namespace esphome {
* pointer. When it is default constructed, it has empty string. You can freely copy or move around this struct, but
* never free its pointer. str() function can be used to export the content as std::string. StringRef is adopted from
* <https://github.com/nghttp2/nghttp2/blob/29cbf8b83ff78faf405d1086b16adc09a8772eca/src/template.h#L376>
*
* A StringRef may carry a null pointer while its length is zero (the generated api messages start their encode only
* string fields that way). Every member treats that as the empty string; only c_str() hands the null pointer on, so
* callers that print or copy through c_str() must check empty() first.
*/
class StringRef {
public:
@@ -78,7 +82,7 @@ class StringRef {
/// True if the view begins with the given prefix (std::string::starts_with-like)
bool starts_with(const StringRef &prefix) const {
return len_ >= prefix.len_ && std::memcmp(base_, prefix.base_, prefix.len_) == 0;
return len_ >= prefix.len_ && (prefix.len_ == 0 || std::memcmp(base_, prefix.base_, prefix.len_) == 0);
}
bool starts_with(const char *prefix) const { return this->starts_with(StringRef(prefix)); }
bool starts_with(const std::string &prefix) const { return this->starts_with(StringRef(prefix)); }
@@ -92,14 +96,15 @@ class StringRef {
return actual;
}
std::string str() const { return std::string(base_, len_); }
std::string str() const { return std::string(base_, len_); } // fine for {nullptr, 0}: nothing is read
const uint8_t *byte() const { return reinterpret_cast<const uint8_t *>(base_); }
operator std::string() const { return str(); }
/// Compare (compatible with std::string::compare)
int compare(const StringRef &other) const {
int result = std::memcmp(base_, other.base_, std::min(len_, other.len_));
size_type common = std::min(len_, other.len_);
int result = common == 0 ? 0 : std::memcmp(base_, other.base_, common);
if (result != 0)
return result;
if (len_ < other.len_)
@@ -258,7 +263,14 @@ inline double stod(const StringRef &str, size_t *pos = nullptr) {
#ifdef USE_JSON
// NOLINTNEXTLINE(readability-identifier-naming)
inline void convertToJson(const StringRef &src, JsonVariant dst) { dst.set(src.c_str()); }
inline void convertToJson(const StringRef &src, JsonVariant dst) {
// Bounded by the view length; a null, empty view becomes "" rather than JSON null
if (src.empty()) {
dst.set("");
return;
}
dst.set(JsonString(src.c_str(), src.size()));
}
#endif // USE_JSON
} // namespace esphome
+127 -253
View File
@@ -28,6 +28,11 @@ class WireType(IntEnum):
END_GROUP = 4 # groups (deprecated)
FIXED32 = 5 # fixed32, sfixed32, float
@property
def cpp_name(self) -> str:
"""The matching constant in proto.h."""
return f"WIRE_TYPE_{self.name}"
# Generate with
# protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto
@@ -126,11 +131,6 @@ def camel_to_snake(name: str) -> str:
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
def force_str(force: bool) -> str:
"""Convert a boolean force value to string format for C++ code."""
return str(force).lower()
def _encode_call(func: str, *args: str, force: bool = False) -> str:
"""Emit one ProtoEncode call; every helper takes the cursor and returns it advanced."""
suffix = "_force" if force else ""
@@ -229,41 +229,23 @@ class TypeInfo(ABC):
def class_member(self) -> str:
return f"{self.cpp_type} {self.field_name}{{{self.default_value}}};"
@property
def decode_varint_content(self) -> str:
content = self.decode_varint
if content is None:
return None
return f"case {self.number}: this->{self.field_name} = {content}; break;"
def decode_case(self, body: str) -> str:
"""Emit one decode_field() case, keyed on the field's wire tag."""
return f"case proto_tag({self.number}, {self.wire_type.cpp_name}):\n" + indent(
f"{body}\nbreak;"
)
decode_varint = None
# Expression that reads this field from `value`; None when the type is never decoded.
decode_expr: str | None = None
def _decode_store(self, expr: str) -> str:
return f"this->{self.field_name} = {expr};"
@property
def decode_length_content(self) -> str:
content = self.decode_length
if content is None:
return None
return f"case {self.number}: this->{self.field_name} = {content}; break;"
decode_length = None
@property
def decode_32bit_content(self) -> str:
content = self.decode_32bit
if content is None:
return None
return f"case {self.number}: this->{self.field_name} = {content}; break;"
decode_32bit = None
@property
def decode_64bit_content(self) -> str:
content = self.decode_64bit
if content is None:
return None
return f"case {self.number}: this->{self.field_name} = {content}; break;"
decode_64bit = None
def decode_content(self) -> str | None:
"""The decode_field() case for this field, or None when it is never decoded."""
expr = self.decode_expr
return None if expr is None else self.decode_case(self._decode_store(expr))
# Mapping from encode_func to raw encode expression template.
# When a forced field has a single-byte tag, the code generator emits
@@ -339,11 +321,12 @@ class TypeInfo(ABC):
)
)
def _encode_fixed32_with_precomputed_tag(self, value_expr: str) -> str | None:
"""Single-byte tag fixed32 write, or None for multi-byte tags."""
def _encode_fixed32_with_precomputed_tag(self, value: str) -> str | None:
"""Single-byte tag fixed32 write, or None for other types and multi-byte tags."""
tag = self.calculate_tag()
if tag >= 128:
if self.fixed32_value_template is None or tag >= 128:
return None
value_expr = self.fixed32_value_template.format(value=value)
if self.force:
return _encode_call("write_tag_and_fixed32", str(tag), value_expr)
return (
@@ -357,14 +340,14 @@ class TypeInfo(ABC):
value = f"this->{self.field_name}"
if result := self._encode_with_precomputed_tag(value):
return result
if self.fixed32_value_template is not None and (
result := self._encode_fixed32_with_precomputed_tag(
self.fixed32_value_template.format(value=value)
)
):
if result := self._encode_fixed32_with_precomputed_tag(value):
return result
return _encode_call(self.encode_func, str(self.number), value, force=self.force)
def encode_element(self, number: int, element: str) -> str:
"""Encode one element of a repeated field; elements are always written."""
return _encode_call(self.encode_func, str(number), element, force=True)
encode_func = None
@classmethod
@@ -583,17 +566,17 @@ def create_field_type_info(
# For messages that decode (SOURCE_CLIENT or SOURCE_BOTH), use pointer
# for zero-copy access to the receive buffer
if needs_decode:
return PointerToBytesBufferType(field, None)
return PointerToBytesBufferType(field, needs_decode)
# For SOURCE_SERVER (encode only), explicit annotation is still needed
if get_field_opt(field, pb.pointer_to_buffer, False):
return PointerToBytesBufferType(field, None)
return PointerToBytesBufferType(field, needs_decode)
return BytesType(field, needs_decode, needs_encode)
# Special handling for string fields - use StringRef for zero-copy
if field.type == 9:
return PointerToStringBufferType(field, None)
return PointerToStringBufferType(field, needs_decode)
validate_field_type(field.type, field.name)
if field.type == 11:
@@ -638,7 +621,6 @@ class DoubleType(FixedSizeTypeMixin, TypeInfo):
# Unsupported but defined for completeness
cpp_type = "double"
default_value = "0.0"
decode_64bit = "value.as_double()"
encode_func = "encode_double"
wire_type = WireType.FIXED64 # Uses wire type 1 according to protobuf spec
@@ -664,7 +646,7 @@ class DoubleType(FixedSizeTypeMixin, TypeInfo):
class FloatType(FixedSizeTypeMixin, TypeInfo):
cpp_type = "float"
default_value = "0.0f"
decode_32bit = "value.as_float()"
decode_expr = "value.as_float()"
encode_func = "encode_float"
wire_type = WireType.FIXED32 # Uses wire type 5
@@ -693,7 +675,7 @@ class Int64Type(VarintTypeMixin, TypeInfo):
cpp_type = "int64_t"
_varint_max_bits = 64
default_value = "0"
decode_varint = "static_cast<int64_t>(value)"
decode_expr = "static_cast<int64_t>(value.as_varint())"
encode_func = "encode_int64"
wire_type = WireType.VARINT # Uses wire type 0
@@ -714,7 +696,7 @@ class UInt64Type(VarintTypeMixin, TypeInfo):
cpp_type = "uint64_t"
_varint_max_bits = 64
default_value = "0"
decode_varint = "value"
decode_expr = "value.as_varint()"
encode_func = "encode_uint64"
wire_type = WireType.VARINT # Uses wire type 0
@@ -749,7 +731,7 @@ class Int32Type(VarintTypeMixin, TypeInfo):
cpp_type = "int32_t"
_varint_max_bits = 64 # int32 is sign-extended to 64 bits in protobuf
default_value = "0"
decode_varint = "static_cast<int32_t>(value)"
decode_expr = "static_cast<int32_t>(value.as_varint())"
encode_func = "encode_int32"
wire_type = WireType.VARINT # Uses wire type 0
@@ -769,7 +751,6 @@ class Int32Type(VarintTypeMixin, TypeInfo):
class Fixed64Type(FixedSizeTypeMixin, TypeInfo):
cpp_type = "uint64_t"
default_value = "0"
decode_64bit = "value.as_fixed64()"
encode_func = "encode_fixed64"
wire_type = WireType.FIXED64 # Uses wire type 1
@@ -795,7 +776,7 @@ class Fixed64Type(FixedSizeTypeMixin, TypeInfo):
class Fixed32Type(FixedSizeTypeMixin, TypeInfo):
cpp_type = "uint32_t"
default_value = "0"
decode_32bit = "value.as_fixed32()"
decode_expr = "value.as_fixed32()"
encode_func = "encode_fixed32"
wire_type = WireType.FIXED32 # Uses wire type 5
@@ -824,7 +805,7 @@ class BoolType(VarintTypeMixin, TypeInfo):
_varint_max_bits = 1
cpp_type = "bool"
default_value = "false"
decode_varint = "value != 0"
decode_expr = "value.as_bool()"
encode_func = "encode_bool"
wire_type = WireType.VARINT # Uses wire type 0
@@ -844,7 +825,7 @@ class StringType(TypeInfo):
default_value = ""
reference_type = "std::string &"
const_reference_type = "const std::string &"
decode_length = "value.as_string()"
decode_expr = "value.as_string()"
encode_func = "encode_string"
wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2
@@ -959,6 +940,9 @@ class MessageType(TypeInfo):
def can_use_dump_field(cls) -> bool:
return False
def encode_element(self, number: int, element: str) -> str:
return _encode_call("encode_sub_message", "buffer", str(number), element)
@property
def cpp_type(self) -> str:
return self._field.type_name[1:]
@@ -985,14 +969,6 @@ class MessageType(TypeInfo):
self.encode_func, "buffer", str(self.number), f"this->{self.field_name}"
)
@property
def decode_length(self) -> str:
# Override to return None for message types because we can't use template-based
# decoding when the specific message type isn't known at compile time.
# Instead, we use the non-template decode_to_message() method which allows
# runtime polymorphism through virtual function calls.
return None
@property
def public_content(self) -> list[str]:
content = [self.class_member]
@@ -1008,19 +984,14 @@ class MessageType(TypeInfo):
)
@property
def decode_length_content(self) -> str:
# Custom decode that doesn't use templates
def decode_content(self) -> str:
body = f"value.decode_to_message(this->{self.field_name});"
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 (
f"case {self.number}:\n"
f" value.decode_to_message(this->{self.field_name});\n"
f" this->has_{self.name} = true;\n"
f" break;"
)
return f"case {self.number}: value.decode_to_message(this->{self.field_name}); break;"
body += f"\nthis->has_{self.name} = true;"
return self.decode_case(body)
def dump(self, name: str) -> str:
return f"{name}.dump_to(out);"
@@ -1059,7 +1030,7 @@ class BytesType(TypeInfo):
reference_type = "std::string &"
const_reference_type = "const std::string &"
encode_func = "encode_bytes"
decode_length = "value.as_string()"
decode_expr = "value.as_string()"
wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2
@property
@@ -1163,16 +1134,12 @@ class PointerToBufferTypeBase(TypeInfo):
def can_use_dump_field(cls) -> bool:
return False
# Only here to make needs_decode required: the null string default keys off it, so a call
# site must not fall back on the base class default
def __init__(
self, field: descriptor.FieldDescriptorProto, size: int | None = None
self, field: descriptor.FieldDescriptorProto, needs_decode: bool
) -> None:
super().__init__(field)
self.array_size = 0
@property
def decode_length(self) -> str | None:
# This is handled in decode_length_content
return None
super().__init__(field, needs_decode)
@property
def wire_type(self) -> WireType:
@@ -1215,12 +1182,11 @@ class PointerToBytesBufferType(PointerToBufferTypeBase):
)
@property
def decode_length_content(self) -> str | None:
return f"""case {self.number}: {{
this->{self.field_name} = value.data();
this->{self.field_name}_len = value.size();
break;
}}"""
def decode_content(self) -> str:
return self.decode_case(
f"this->{self.field_name} = value.data();\n"
f"this->{self.field_name}_len = value.size();",
)
def dump(self, name: str) -> str:
return (
@@ -1254,14 +1220,28 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
def can_use_dump_field(cls) -> bool:
return True
@property
def _starts_null(self) -> bool:
"""A field that is only encoded, and skipped when empty, never has its pointer read
before it is set, so it can default to a null StringRef and the message constructs as
one zero fill. Any encode path that copies unconditionally must check this."""
return not self._needs_decode and not self.force
@property
def public_content(self) -> list[str]:
if self._starts_null:
return [
f"StringRef {self.field_name}{{nullptr, 0}}; // null until set, encode only"
]
return [f"StringRef {self.field_name}{{}};"]
@property
def encode_content(self) -> str:
max_len = self.max_data_length
if max_len is not None and max_len < 128 and self.force:
assert not self._starts_null, (
"unconditional copy of a field that may start null"
)
tag = self.calculate_tag()
if tag < 128:
return _encode_call(
@@ -1271,6 +1251,9 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
f"this->{self.field_name}.c_str()",
f"this->{self.field_name}.size()",
):
assert not self._starts_null, (
"unconditional copy of a field that may start null"
)
return result
return _encode_call(
"encode_string",
@@ -1280,11 +1263,10 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
)
@property
def decode_length_content(self) -> str | None:
return f"""case {self.number}: {{
this->{self.field_name} = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
break;
}}"""
def decode_content(self) -> str:
return self.decode_case(
f"this->{self.field_name} = StringRef(value.data(), value.size());",
)
def dump(self, name: str) -> str:
# Not used since we use dump_field, but required by abstract base class
@@ -1353,14 +1335,13 @@ class PackedBufferTypeInfo(TypeInfo):
]
@property
def decode_length_content(self) -> str:
def decode_content(self) -> str:
"""Store pointer to buffer and calculate count of packed varints."""
return f"""case {self.number}: {{
this->{self.field_name}_data_ = value.data();
this->{self.field_name}_length_ = value.size();
this->{self.field_name}_count_ = count_packed_varints(value.data(), value.size());
break;
}}"""
return self.decode_case(
f"this->{self.field_name}_data_ = value.data();\n"
f"this->{self.field_name}_length_ = value.size();\n"
f"this->{self.field_name}_count_ = count_packed_varints(value.data(), value.size());",
)
@property
def encode_content(self) -> str:
@@ -1445,17 +1426,11 @@ class FixedArrayBytesType(TypeInfo):
]
@property
def decode_length_content(self) -> str:
o = f"case {self.number}: {{\n"
o += " const std::string &data_str = value.as_string();\n"
o += f" this->{self.field_name}_len = data_str.size();\n"
o += f" if (this->{self.field_name}_len > {self.array_size}) {{\n"
o += f" this->{self.field_name}_len = {self.array_size};\n"
o += " }\n"
o += f" memcpy(this->{self.field_name}, data_str.data(), this->{self.field_name}_len);\n"
o += " break;\n"
o += "}"
return o
def decode_content(self) -> str:
return self.decode_case(
f"this->{self.field_name}_len = std::min<size_t>(value.size(), {self.array_size});\n"
f"memcpy(this->{self.field_name}, value.data(), this->{self.field_name}_len);",
)
@property
def encode_content(self) -> str:
@@ -1518,7 +1493,7 @@ class UInt32Type(VarintTypeMixin, TypeInfo):
cpp_type = "uint32_t"
_varint_max_bits = 32
default_value = "0"
decode_varint = "value"
decode_expr = "value.as_varint()"
encode_func = "encode_uint32"
wire_type = WireType.VARINT # Uses wire type 0
@@ -1541,13 +1516,21 @@ class UInt32Type(VarintTypeMixin, TypeInfo):
class EnumType(VarintTypeMixin, TypeInfo):
_varint_max_bits = 32
def encode_element(self, number: int, element: str) -> str:
return _encode_call(
self.encode_func,
str(number),
f"static_cast<uint32_t>({element})",
force=True,
)
@property
def cpp_type(self) -> str:
return f"enums::{self._field.type_name[1:]}"
@property
def decode_varint(self) -> str:
return f"static_cast<{self.cpp_type}>(value)"
def decode_expr(self) -> str:
return f"static_cast<{self.cpp_type}>(value.as_varint())"
default_value = ""
wire_type = WireType.VARINT # Uses wire type 0
@@ -1594,7 +1577,7 @@ class EnumType(VarintTypeMixin, TypeInfo):
class SFixed32Type(FixedSizeTypeMixin, TypeInfo):
cpp_type = "int32_t"
default_value = "0"
decode_32bit = "value.as_sfixed32()"
decode_expr = "value.as_sfixed32()"
encode_func = "encode_sfixed32"
wire_type = WireType.FIXED32 # Uses wire type 5
@@ -1620,7 +1603,6 @@ class SFixed32Type(FixedSizeTypeMixin, TypeInfo):
class SFixed64Type(FixedSizeTypeMixin, TypeInfo):
cpp_type = "int64_t"
default_value = "0"
decode_64bit = "value.as_sfixed64()"
encode_func = "encode_sfixed64"
wire_type = WireType.FIXED64 # Uses wire type 1
@@ -1647,7 +1629,7 @@ class SInt32Type(VarintTypeMixin, TypeInfo):
cpp_type = "int32_t"
_varint_max_bits = 32 # zigzag encoding keeps it 32-bit
default_value = "0"
decode_varint = "decode_zigzag32(static_cast<uint32_t>(value))"
decode_expr = "decode_zigzag32(static_cast<uint32_t>(value.as_varint()))"
encode_func = "encode_sint32"
wire_type = WireType.VARINT # Uses wire type 0
@@ -1668,7 +1650,7 @@ class SInt64Type(VarintTypeMixin, TypeInfo):
cpp_type = "int64_t"
_varint_max_bits = 64
default_value = "0"
decode_varint = "decode_zigzag64(value)"
decode_expr = "decode_zigzag64(value.as_varint())"
encode_func = "encode_sint64"
wire_type = WireType.VARINT # Uses wire type 0
@@ -1821,23 +1803,11 @@ class FixedArrayRepeatedType(TypeInfo):
def _encode_element(self, element: str) -> str:
"""Helper to generate encode statement for a single element."""
if isinstance(self._ti, EnumType):
return _encode_call(
self._ti.encode_func,
str(self.number),
f"static_cast<uint32_t>({element})",
force=True,
if isinstance(self._ti, MessageType) and _is_inline_encode(self._ti.cpp_type):
return _generate_inline_encode_block(
self.number, self._ti.cpp_type, element
)
# Repeated message elements use encode_sub_message (force=true is default)
if isinstance(self._ti, MessageType):
if _is_inline_encode(self._ti.cpp_type):
return _generate_inline_encode_block(
self.number, self._ti.cpp_type, element
)
return _encode_call(
"encode_sub_message", "buffer", str(self.number), element
)
return _encode_call(self._ti.encode_func, str(self.number), element, force=True)
return self._ti.encode_element(self.number, element)
@property
def cpp_type(self) -> str:
@@ -2131,55 +2101,23 @@ class RepeatedTypeInfo(TypeInfo):
return self._ti.wire_type
@property
def decode_varint_content(self) -> str:
# Pointer fields don't support decoding
if self._use_pointer:
return None
content = self._ti.decode_varint
if content is None:
return None
return (
f"case {self.number}: this->{self.field_name}.push_back({content}); break;"
)
def decode_expr(self) -> str | None:
return self._ti.decode_expr
def _decode_store(self, expr: str) -> str:
return f"this->{self.field_name}.push_back({expr});"
@property
def decode_length_content(self) -> str:
def decode_content(self) -> str | None:
# Pointer fields don't support decoding
if self._use_pointer:
return None
content = self._ti.decode_length
if content is None and isinstance(self._ti, MessageType):
# Special handling for non-template message decoding
return f"case {self.number}: this->{self.field_name}.emplace_back(); value.decode_to_message(this->{self.field_name}.back()); break;"
if content is None:
return None
return (
f"case {self.number}: this->{self.field_name}.push_back({content}); break;"
)
@property
def decode_32bit_content(self) -> str:
# Pointer fields don't support decoding
if self._use_pointer:
return None
content = self._ti.decode_32bit
if content is None:
return None
return (
f"case {self.number}: this->{self.field_name}.push_back({content}); break;"
)
@property
def decode_64bit_content(self) -> str:
# Pointer fields don't support decoding
if self._use_pointer:
return None
content = self._ti.decode_64bit
if content is None:
return None
return (
f"case {self.number}: this->{self.field_name}.push_back({content}); break;"
)
if isinstance(self._ti, MessageType):
return self.decode_case(
f"this->{self.field_name}.emplace_back();\n"
f"value.decode_to_message(this->{self.field_name}.back());"
)
return super().decode_content
@property
def _ti_is_bool(self) -> bool:
@@ -2187,20 +2125,7 @@ class RepeatedTypeInfo(TypeInfo):
return isinstance(self._ti, BoolType)
def _encode_element_call(self, element: str) -> str:
"""Helper to generate encode call for a single element."""
if isinstance(self._ti, EnumType):
return _encode_call(
self._ti.encode_func,
str(self.number),
f"static_cast<uint32_t>({element})",
force=True,
)
# Repeated message elements use encode_sub_message (force=true is default)
if isinstance(self._ti, MessageType):
return _encode_call(
"encode_sub_message", "buffer", str(self.number), element
)
return _encode_call(self._ti.encode_func, str(self.number), element, force=True)
return self._ti.encode_element(self.number, element)
@property
def encode_content(self) -> str:
@@ -2595,10 +2520,7 @@ def build_message_type(
) -> tuple[str, str, str]:
public_content: list[str] = []
protected_content: list[str] = []
decode_varint: list[str] = []
decode_length: list[str] = []
decode_32bit: list[str] = []
decode_64bit: list[str] = []
decode: list[str] = []
encode: list[str] = []
dump: list[str] = []
size_calc: list[str] = []
@@ -2727,22 +2649,8 @@ def build_message_type(
if field.options.HasExtension(pb.field_ifdef):
field_ifdef = field.options.Extensions[pb.field_ifdef]
if ti.decode_varint_content:
decode_varint.extend(
wrap_with_ifdef(ti.decode_varint_content, field_ifdef)
)
if ti.decode_length_content:
decode_length.extend(
wrap_with_ifdef(ti.decode_length_content, field_ifdef)
)
if ti.decode_32bit_content:
decode_32bit.extend(
wrap_with_ifdef(ti.decode_32bit_content, field_ifdef)
)
if ti.decode_64bit_content:
decode_64bit.extend(
wrap_with_ifdef(ti.decode_64bit_content, field_ifdef)
)
if case := ti.decode_content:
decode.extend(wrap_with_ifdef(case, field_ifdef))
if ti.dump_content:
# Check for field_ifdef option for dump as well
field_ifdef = None
@@ -2752,49 +2660,15 @@ def build_message_type(
dump.extend(wrap_with_ifdef(ti.dump_content, field_ifdef))
cpp = ""
if decode_varint:
o = f"bool {desc.name}::decode_varint(uint32_t field_id, proto_varint_value_t value) {{\n"
o += " switch (field_id) {\n"
o += indent("\n".join(decode_varint), " ") + "\n"
o += " default: return false;\n"
if decode:
o = f"void {desc.name}::decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {{\n"
o += " const ProtoFieldValue value(data, scalar);\n"
o += " switch (tag) {\n"
o += indent("\n".join(decode), " ") + "\n"
o += " }\n"
o += " return true;\n"
o += "}\n"
cpp += o
prot = "bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;"
protected_content.insert(0, prot)
if decode_length:
o = f"bool {desc.name}::decode_length(uint32_t field_id, ProtoLengthDelimited value) {{\n"
o += " switch (field_id) {\n"
o += indent("\n".join(decode_length), " ") + "\n"
o += " default: return false;\n"
o += " }\n"
o += " return true;\n"
o += "}\n"
cpp += o
prot = "bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;"
protected_content.insert(0, prot)
if decode_32bit:
o = f"bool {desc.name}::decode_32bit(uint32_t field_id, Proto32Bit value) {{\n"
o += " switch (field_id) {\n"
o += indent("\n".join(decode_32bit), " ") + "\n"
o += " default: return false;\n"
o += " }\n"
o += " return true;\n"
o += "}\n"
cpp += o
prot = "bool decode_32bit(uint32_t field_id, Proto32Bit value) override;"
protected_content.insert(0, prot)
if decode_64bit:
o = f"bool {desc.name}::decode_64bit(uint32_t field_id, Proto64Bit value) {{\n"
o += " switch (field_id) {\n"
o += indent("\n".join(decode_64bit), " ") + "\n"
o += " default: return false;\n"
o += " }\n"
o += " return true;\n"
o += "}\n"
cpp += o
prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;"
prot = "void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;"
protected_content.insert(0, prot)
# Generate custom decode() override for messages with FixedVector fields
@@ -249,7 +249,7 @@ static APIBuffer build_infrared_rf_transmit_wire() {
std::memcpy(bytes + len, packed, packed_len);
len += packed_len;
// field 6: modulation = 1 (non-zero so it's actually emitted and exercises
// decode_varint for this field, matching the documented layout above).
// decode_field for this field, matching the documented layout above).
put_byte(0x30);
put_varint(1);
+43
View File
@@ -59,4 +59,47 @@ TEST(StringRefStartsWith, RefOverloadComparesOnlyTheViewedLength) {
EXPECT_TRUE(ref.starts_with(prefix));
}
// The generated api messages start their encode only string fields as a null pointer with zero
// length; every member must treat that exactly like the default constructed empty string.
TEST(StringRefNullEmpty, BehavesAsEmptyString) {
const StringRef null_empty{nullptr, 0};
const StringRef empty;
EXPECT_TRUE(null_empty.empty());
EXPECT_EQ(null_empty.size(), 0u);
EXPECT_EQ(null_empty.c_str(), nullptr);
EXPECT_TRUE(null_empty == empty);
EXPECT_TRUE(null_empty == "");
EXPECT_TRUE(null_empty == std::string());
EXPECT_EQ(null_empty.compare(empty), 0);
EXPECT_EQ(null_empty.compare(""), 0);
EXPECT_LT(null_empty.compare("a"), 0);
EXPECT_TRUE(null_empty.starts_with(""));
EXPECT_FALSE(null_empty.starts_with("a"));
EXPECT_EQ(null_empty.str(), std::string());
EXPECT_EQ(null_empty.substr(0), std::string());
EXPECT_EQ(null_empty.find('a'), std::string::npos);
EXPECT_EQ(null_empty.find("a"), std::string::npos);
char buf[4] = "xyz";
EXPECT_EQ(null_empty.copy(buf, sizeof(buf)), 0u);
EXPECT_EQ(null_empty.begin(), null_empty.end());
}
TEST(StringRefNullEmpty, ComparesAgainstText) {
const StringRef null_empty{nullptr, 0};
const StringRef text("abc", 3);
EXPECT_FALSE(null_empty == text);
EXPECT_FALSE(text == null_empty);
EXPECT_LT(null_empty.compare(text), 0);
EXPECT_GT(text.compare(null_empty), 0);
EXPECT_TRUE(text.starts_with(null_empty));
}
TEST(StringRefNullEmpty, TwoNullViewsAreEqual) {
const StringRef a{nullptr, 0};
const StringRef b{nullptr, 0};
EXPECT_TRUE(a == b);
EXPECT_EQ(a.compare(b), 0);
EXPECT_TRUE(a.starts_with(b));
}
} // namespace esphome::core::testing
@@ -0,0 +1,43 @@
esphome:
name: api-decode-wire-types-test
host:
api:
logger:
level: DEBUG
switch:
- platform: template
name: "Wire Switch"
optimistic: true
output:
- platform: template
id: wire_dim
type: float
write_action:
- lambda: ""
light:
- platform: monochromatic
name: "Wire Light"
output: wire_dim
default_transition_length: 0s
effects:
- pulse:
name: Pulse
text:
- platform: template
name: "Wire Text"
optimistic: true
mode: text
min_length: 0
max_length: 255
number:
- platform: template
name: "Wire Number"
optimistic: true
min_value: -1000
max_value: 1000
step: 0.5
+5 -4
View File
@@ -125,11 +125,12 @@ class RawApiClient:
await self.read_until_frame(MESSAGE_TYPE_OF[api_pb2.HelloResponse])
async def send_message(self, msg: message.Message) -> None:
await self.send_raw(MESSAGE_TYPE_OF[type(msg)], msg.SerializeToString())
async def send_raw(self, msg_type: int, payload: bytes) -> None:
"""Send a frame with a hand built payload, for shapes protobuf will not serialize."""
loop = asyncio.get_running_loop()
await loop.sock_sendall(
self._sock,
encode_frame(MESSAGE_TYPE_OF[type(msg)], msg.SerializeToString()),
)
await loop.sock_sendall(self._sock, encode_frame(msg_type, payload))
async def read_until_frame(self, msg_type: int, timeout: float = 10.0) -> None:
"""Read until at least one frame of msg_type has been received."""
@@ -0,0 +1,142 @@
"""decode_field() must take fields that match their declared wire type, drop the ones that do
not, skip unknown fields, and handle two byte tags, varints and length prefixes."""
from __future__ import annotations
from collections.abc import Callable
import struct
from aioesphomeapi import (
EntityState,
LightState,
NumberState,
SwitchState,
TextState,
api_pb2,
)
import pytest
from .raw_api_client import MESSAGE_TYPE_OF, RawApiClient, encode_varint
from .state_utils import InitialStateHelper, StateWaiter, require_entity
from .types import APIClientConnectedFactory, RunCompiledFunction
SWITCH_COMMAND = MESSAGE_TYPE_OF[api_pb2.SwitchCommandRequest]
WIRE_VARINT, WIRE_LENGTH, WIRE_FIXED32 = 0, 2, 5
def tag(field: int, wire_type: int) -> bytes:
return encode_varint((field << 3) | wire_type)
@pytest.mark.asyncio
async def test_api_decode_wire_types(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
unused_tcp_port: int,
) -> None:
async with (
run_compiled(yaml_config),
api_client_connected() as client,
RawApiClient(unused_tcp_port) as raw,
):
entities, _ = await client.list_entities_services()
switch = require_entity(entities, "wire_switch")
light = require_entity(entities, "wire_light")
text = require_entity(entities, "wire_text")
number = require_entity(entities, "wire_number")
key = tag(1, WIRE_FIXED32) + struct.pack("<I", switch.key)
on, off = tag(2, WIRE_VARINT) + b"\x01", tag(2, WIRE_VARINT) + b"\x00"
switch_states: list[bool] = []
waiter = StateWaiter()
def on_state(state: EntityState) -> None:
if isinstance(state, SwitchState) and state.key == switch.key:
switch_states.append(state.state)
waiter.on_state(state)
def switch_is(value: bool) -> Callable[[EntityState], bool]:
return lambda s: (
isinstance(s, SwitchState) and s.key == switch.key and s.state is value
)
def number_is(value: float) -> Callable[[EntityState], bool]:
return lambda s: (
isinstance(s, NumberState) and s.key == number.key and s.state == value
)
initial = InitialStateHelper(entities)
client.subscribe_states(initial.on_state_wrapper(on_state))
await initial.wait_for_initial_states()
await raw.connect()
# A well formed command: fixed32 key, varint state
await raw.send_raw(SWITCH_COMMAND, key + on)
await waiter.expect(switch_is(True))
await raw.send_raw(SWITCH_COMMAND, key + off)
await waiter.expect(switch_is(False))
# The same field with the wrong wire type is dropped, and a varint key never matches an
# entity; each of these would turn the switch on if the payload were read as a varint
seen = len(switch_states)
await raw.send_raw(SWITCH_COMMAND, key + tag(2, WIRE_LENGTH) + b"\x01\x01")
await raw.send_raw(
SWITCH_COMMAND, key + tag(2, WIRE_FIXED32) + b"\x01\x00\x00\x00"
)
await raw.send_raw(
SWITCH_COMMAND, tag(1, WIRE_VARINT) + encode_varint(switch.key) + on
)
# Ordered on the raw socket itself: this frame cannot be parsed before the bad ones, so
# the only switch state since the marker must be the one it produces
await raw.send_raw(SWITCH_COMMAND, key + on)
await waiter.expect(switch_is(True), label="switch on after wrong wire types")
assert switch_states[seen:] == [True]
await raw.send_raw(SWITCH_COMMAND, key + off)
await waiter.expect(switch_is(False))
# Truncated bodies stop the decode loop without taking the connection down: a tag with its
# continuation bit set and nothing after it, a length prefix past the end of the payload,
# and a fixed32 with two of its four bytes
seen = len(switch_states)
await raw.send_raw(SWITCH_COMMAND, key + b"\x80")
await raw.send_raw(SWITCH_COMMAND, key + tag(2, WIRE_LENGTH) + b"\x7f" + b"ab")
await raw.send_raw(SWITCH_COMMAND, tag(1, WIRE_FIXED32) + b"\x01\x02")
await raw.send_raw(SWITCH_COMMAND, key + on)
await waiter.expect(switch_is(True), label="switch on after truncated frames")
assert switch_states[seen:] == [True]
await raw.send_raw(SWITCH_COMMAND, key + off)
await waiter.expect(switch_is(False))
# A negative number goes through the fixed32 float path of a normal client
client.number_command(number.key, -77.5)
await waiter.expect(number_is(-77.5))
# An unknown field ahead of the known ones is skipped; field 200 needs a two byte tag
await raw.send_raw(
SWITCH_COMMAND, tag(200, WIRE_VARINT) + encode_varint(300) + key + on
)
await waiter.expect(switch_is(True))
# Two byte tags (effect fields 18 and 19) and a two byte varint (300 ms transition)
client.light_command(
light.key, state=True, brightness=0.5, transition_length=0.3, effect="Pulse"
)
await waiter.expect(
lambda s: (
isinstance(s, LightState) and s.key == light.key and s.effect == "Pulse"
)
)
client.light_command(light.key, effect="None", state=False)
await waiter.expect(
lambda s: isinstance(s, LightState) and s.key == light.key and not s.state
)
# A string whose length prefix needs two varint bytes
long_text = "w" * 200
client.text_command(text.key, long_text)
await waiter.expect(
lambda s: (
isinstance(s, TextState) and s.key == text.key and s.state == long_text
)
)
@@ -18,7 +18,9 @@ sys.path.insert(0, str(Path(__file__).parents[4] / "script" / "api_protobuf"))
import aioesphomeapi.api_options_pb2 as pb # noqa: E402
from api_protobuf import ( # noqa: E402
MAX_MESSAGE_ID,
SOURCE_CLIENT,
_make_ifdef_line,
build_message_type,
create_field_type_info,
get_varint64_ifdef,
validate_message_id,
@@ -36,12 +38,15 @@ def _file_with_messages(
file_desc = descriptor_pb2.FileDescriptorProto(name="test.proto")
for name, field_type, deprecated in messages:
msg = file_desc.message_type.add(name=name)
field = msg.field.add(name="value", number=1, type=field_type)
field = msg.field.add()
field.CopyFrom(_field(field_type))
field.options.deprecated = deprecated
return file_desc
UINT64 = descriptor_pb2.FieldDescriptorProto.TYPE_UINT64
MESSAGE = descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE
DOUBLE = descriptor_pb2.FieldDescriptorProto.TYPE_DOUBLE
INT64 = descriptor_pb2.FieldDescriptorProto.TYPE_INT64
SINT64 = descriptor_pb2.FieldDescriptorProto.TYPE_SINT64
UINT32 = descriptor_pb2.FieldDescriptorProto.TYPE_UINT32
@@ -182,3 +187,124 @@ def test_multi_byte_tag_fixed32_falls_back_to_the_generic_helper(
content = _encode_field(field_type, number=16)
assert "write_tag_and_fixed32" not in content, content
assert content.startswith("pos = ProtoEncode::encode_"), content
def _decode_case(field_type: int, number: int, *, repeated: bool = False) -> str:
"""Return the decode_field() case the generator emits for one decoded field."""
field = _field(field_type, number, repeated=repeated)
if field_type == MESSAGE:
field.type_name = ".Sub"
return create_field_type_info(
field, needs_decode=True, needs_encode=False
).decode_content
@pytest.mark.parametrize(
("needs_decode", "force", "member"),
[
(False, False, "StringRef value{nullptr, 0}; // null until set, encode only"),
(True, False, "StringRef value{};"),
(False, True, "StringRef value{};"),
],
)
def test_string_fields_default_to_null_only_when_never_read(
needs_decode: bool, force: bool, member: str
) -> None:
"""Only a string that is neither decoded nor force encoded may start as a null StringRef."""
ti = create_field_type_info(
_field(STRING, force=force), needs_decode=needs_decode, needs_encode=True
)
assert ti.public_content == [member]
@pytest.mark.parametrize(
("field_type", "number", "wire_type", "accessor"),
[
(UINT32, 2, "WIRE_TYPE_VARINT", "value.as_varint()"),
(BOOL, 3, "WIRE_TYPE_VARINT", "value.as_bool()"),
(STRING, 1, "WIRE_TYPE_LENGTH_DELIMITED", "value.data()"),
(FLOAT, 4, "WIRE_TYPE_FIXED32", "value.as_float()"),
(FIXED32, 5, "WIRE_TYPE_FIXED32", "value.as_fixed32()"),
],
)
def test_decode_cases_carry_field_number_and_wire_type(
field_type: int, number: int, wire_type: str, accessor: str
) -> None:
"""Each decoded field yields one case keyed on its number and declared wire type."""
case = _decode_case(field_type, number)
lines = case.splitlines()
assert lines[0] == f"case proto_tag({number}, {wire_type}):", case
assert accessor in lines[1], case
assert lines[-1].strip() == "break;", case
@pytest.mark.parametrize(
("field_type", "repeated", "wire_type", "store"),
[
(UINT32, True, "WIRE_TYPE_VARINT", "this->value.push_back(value.as_varint());"),
(
STRING,
True,
"WIRE_TYPE_LENGTH_DELIMITED",
"this->value.push_back(value.as_string());",
),
(
MESSAGE,
False,
"WIRE_TYPE_LENGTH_DELIMITED",
"value.decode_to_message(this->value);",
),
(
MESSAGE,
True,
"WIRE_TYPE_LENGTH_DELIMITED",
"value.decode_to_message(this->value.back());",
),
],
)
def test_repeated_and_message_fields_decode_through_the_same_case_shape(
field_type: int, repeated: bool, wire_type: str, store: str
) -> None:
"""Repeated and sub message fields land in the one switch with their own store."""
case = _decode_case(field_type, 7, repeated=repeated)
lines = case.splitlines()
assert lines[0] == f"case proto_tag(7, {wire_type}):", case
assert store in case, case
if field_type == MESSAGE and repeated:
assert "this->value.emplace_back();" in case, case
assert lines[-1].strip() == "break;", case
def test_a_fixed64_field_fails_at_generation_time() -> None:
"""The decode loop has no 64 bit wire type path, so such a field must never reach it silently."""
desc = descriptor_pb2.DescriptorProto(name="Wide")
desc.field.add(name="ratio", number=1, type=DOUBLE)
with pytest.raises(
ValueError, match="64-bit type 'double' .*ratio.* not supported"
):
build_message_type(desc, {}, {"Wide": SOURCE_CLIENT})
def test_message_gets_a_single_decode_field_override() -> None:
"""All wire types of a decoded message land in one decode_field() switch."""
desc = descriptor_pb2.DescriptorProto(name="Mixed")
desc.field.add(name="name", number=1, type=STRING)
desc.field.add(name="count", number=2, type=UINT32)
desc.field.add(name="level", number=3, type=FLOAT)
header, cpp, _ = build_message_type(desc, {}, {"Mixed": SOURCE_CLIENT})
decl = "void decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) override;"
assert header.count(decl) == 1
assert (
cpp.count(
"void Mixed::decode_field(uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {"
)
== 1
)
assert "switch (tag) {" in cpp
assert "const ProtoFieldValue value(data, scalar);" in cpp
for number, wire_type in (
(1, "WIRE_TYPE_LENGTH_DELIMITED"),
(2, "WIRE_TYPE_VARINT"),
(3, "WIRE_TYPE_FIXED32"),
):
assert f"case proto_tag({number}, {wire_type}):" in cpp, cpp