Compare commits

..

1 Commits

Author SHA1 Message Date
J. Nick Koston
80927f0f80 [core] Make LOG_ENTITY_ICON a no-op when icons are compiled out
When USE_ENTITY_ICON is not defined, LOG_ENTITY_ICON and log_entity_icon
are now completely compiled out rather than calling a function that
checks an always-empty icon reference.
2026-02-12 18:29:55 -06:00
16 changed files with 41 additions and 121 deletions

View File

@@ -57,14 +57,8 @@ def maybe_conf(conf, *validators):
return validate
def register_action(
name: str,
action_type: MockObjClass,
schema: cv.Schema,
*,
deferred: bool = False,
):
return ACTION_REGISTRY.register(name, action_type, schema, deferred=deferred)
def register_action(name: str, action_type: MockObjClass, schema: cv.Schema):
return ACTION_REGISTRY.register(name, action_type, schema)
def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema):
@@ -341,10 +335,7 @@ async def component_is_idle_condition_to_code(
@register_action(
"delay",
DelayAction,
cv.templatable(cv.positive_time_period_milliseconds),
deferred=True,
"delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
)
async def delay_action_to_code(
config: ConfigType,
@@ -454,7 +445,7 @@ _validate_wait_until = cv.maybe_simple_value(
)
@register_action("wait_until", WaitUntilAction, _validate_wait_until, deferred=True)
@register_action("wait_until", WaitUntilAction, _validate_wait_until)
async def wait_until_action_to_code(
config: ConfigType,
action_id: ID,
@@ -587,26 +578,6 @@ async def build_condition_list(
return conditions
def has_deferred_actions(actions: ConfigType) -> bool:
"""Check if a validated action list contains any deferred actions.
Deferred actions (delay, wait_until, script.wait) store trigger args
for later execution, making non-owning types like StringRef unsafe.
"""
if isinstance(actions, list):
return any(has_deferred_actions(item) for item in actions)
if isinstance(actions, dict):
for key in actions:
if key in ACTION_REGISTRY and ACTION_REGISTRY[key].deferred:
return True
return any(
has_deferred_actions(v)
for v in actions.values()
if isinstance(v, (list, dict))
)
return False
async def build_automation(
trigger: MockObj, args: TemplateArgsType, config: ConfigType
) -> MockObj:

View File

@@ -76,7 +76,7 @@ SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = {
"bool": cg.bool_,
"int": cg.int32,
"float": cg.float_,
"string": cg.StringRef,
"string": cg.std_string,
"bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"),
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
"float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"),
@@ -380,16 +380,9 @@ async def to_code(config: ConfigType) -> None:
if is_optional:
func_args.append((cg.bool_, "return_response"))
# Check if action chain has deferred actions that would make
# non-owning StringRef dangle (rx_buf_ reused after delay)
has_deferred = automation.has_deferred_actions(conf.get(CONF_THEN, []))
service_arg_names: list[str] = []
for name, var_ in conf[CONF_VARIABLES].items():
native = SERVICE_ARG_NATIVE_TYPES[var_]
# Fall back to std::string for string args if deferred actions exist
if has_deferred and native is cg.StringRef:
native = cg.std_string
service_template_args.append(native)
func_args.append((native, name))
service_arg_names.append(name)

View File

@@ -824,7 +824,7 @@ message HomeAssistantStateResponse {
option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
string entity_id = 1;
string state = 2 [(null_terminate) = true];
string state = 2;
string attribute = 3;
}
@@ -882,7 +882,7 @@ message ExecuteServiceArgument {
bool bool_ = 1;
int32 legacy_int = 2;
float float_ = 3;
string string_ = 4 [(null_terminate) = true];
string string_ = 4;
// ESPHome 1.14 (api v1.3) make int a signed value
sint32 int_ = 5;
repeated bool bool_array = 6 [packed=false, (fixed_vector) = true];

View File

@@ -1683,18 +1683,31 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
}
for (auto &it : this->parent_->get_state_subs()) {
if (msg.entity_id != it.entity_id) {
// Compare entity_id: check length matches and content matches
size_t entity_id_len = strlen(it.entity_id);
if (entity_id_len != msg.entity_id.size() ||
memcmp(it.entity_id, msg.entity_id.c_str(), msg.entity_id.size()) != 0) {
continue;
}
// Compare attribute: either both have matching attribute, or both have none
// it.attribute can be nullptr (meaning no attribute filter)
if (it.attribute != nullptr ? msg.attribute != it.attribute : !msg.attribute.empty()) {
size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0;
if (sub_attr_len != msg.attribute.size() ||
(sub_attr_len > 0 && memcmp(it.attribute, msg.attribute.c_str(), sub_attr_len) != 0)) {
continue;
}
// msg.state is already null-terminated in-place after protobuf decode
it.callback(msg.state);
// Create null-terminated state for callback (parse_number needs null-termination)
// HA state max length is 255 characters, but attributes can be much longer
// Use stack buffer for common case (states), heap fallback for large attributes
size_t state_len = msg.state.size();
SmallBufferWithHeapFallback<MAX_STATE_LEN + 1> state_buf_alloc(state_len + 1);
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get());
if (state_len > 0) {
memcpy(state_buf, msg.state.c_str(), state_len);
}
state_buf[state_len] = '\0';
it.callback(StringRef(state_buf, state_len));
}
}
#endif

View File

@@ -201,10 +201,9 @@ APIError APINoiseFrameHelper::try_read_frame_() {
return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
}
// Reserve space for body (+1 for null terminator so protobuf StringRef fields
// can be safely null-terminated in-place after decode)
if (this->rx_buf_.size() != msg_size + 1) {
this->rx_buf_.resize(msg_size + 1);
// Reserve space for body
if (this->rx_buf_.size() != msg_size) {
this->rx_buf_.resize(msg_size);
}
if (rx_buf_len_ < msg_size) {

View File

@@ -163,10 +163,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
}
// header reading done
// Reserve space for body (+1 for null terminator so protobuf StringRef fields
// can be safely null-terminated in-place after decode)
if (this->rx_buf_.size() != this->rx_header_parsed_len_ + 1) {
this->rx_buf_.resize(this->rx_header_parsed_len_ + 1);
// Reserve space for body
if (this->rx_buf_.size() != this->rx_header_parsed_len_) {
this->rx_buf_.resize(this->rx_header_parsed_len_);
}
if (rx_buf_len_ < rx_header_parsed_len_) {

View File

@@ -90,13 +90,4 @@ extend google.protobuf.FieldOptions {
// - uint16_t <field>_length_{0};
// - uint16_t <field>_count_{0};
optional bool packed_buffer = 50015 [default=false];
// null_terminate: Write a null byte after string data in the decode buffer.
// When set on a string field in a SOURCE_CLIENT (decodable) message, the
// generated decode() override writes '\0' at data[length] after decoding.
// This makes the StringRef safe for c_str() usage without copying.
// Safe because: (1) frame helpers reserve +1 byte in rx_buf_, and
// (2) the overwritten byte was already consumed during decode.
// Only mark fields that actually need null-terminated access.
optional bool null_terminate = 50016 [default=false];
}

View File

@@ -953,12 +953,6 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel
}
return true;
}
void HomeAssistantStateResponse::decode(const uint8_t *buffer, size_t length) {
ProtoDecodableMessage::decode(buffer, length);
if (!this->state.empty()) {
const_cast<char *>(this->state.c_str())[this->state.size()] = '\0';
}
}
#endif
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
@@ -1063,9 +1057,6 @@ void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
uint32_t count_string_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 9);
this->string_array.init(count_string_array);
ProtoDecodableMessage::decode(buffer, length);
if (!this->string_.empty()) {
const_cast<char *>(this->string_.c_str())[this->string_.size()] = '\0';
}
}
bool ExecuteServiceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {

View File

@@ -1095,7 +1095,6 @@ class HomeAssistantStateResponse final : public ProtoDecodableMessage {
StringRef entity_id{};
StringRef state{};
StringRef attribute{};
void decode(const uint8_t *buffer, size_t length) override;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif

View File

@@ -1,6 +1,5 @@
#include "user_services.h"
#include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
namespace esphome::api {
@@ -12,8 +11,6 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
}
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
// Zero-copy StringRef version for YAML-generated services (string_ is null-terminated after decode)
template<> StringRef get_execute_arg_value<StringRef>(const ExecuteServiceArgument &arg) { return arg.string_; }
// Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
@@ -64,8 +61,6 @@ template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SER
template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; }
template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
// Zero-copy StringRef version for YAML-generated services
template<> enums::ServiceArgType to_service_arg_type<StringRef>() { return enums::SERVICE_ARG_TYPE_STRING; }
// Legacy std::vector versions for external components using custom_api_device.h
template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }

View File

@@ -219,7 +219,6 @@ async def script_stop_action_to_code(config, action_id, template_arg, args):
"script.wait",
ScriptWaitAction,
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}),
deferred=True,
)
async def script_wait_action_to_code(config, action_id, template_arg, args):
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])

View File

@@ -152,11 +152,13 @@ void EntityBase_UnitOfMeasurement::set_unit_of_measurement(const char *unit_of_m
this->unit_of_measurement_ = unit_of_measurement;
}
#ifdef USE_ENTITY_ICON
void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) {
if (!obj.get_icon_ref().empty()) {
ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj.get_icon_ref().c_str());
}
}
#endif
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj) {
if (!obj.get_device_class_ref().empty()) {

View File

@@ -231,8 +231,12 @@ class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming)
};
/// Log entity icon if set (for use in dump_config)
#ifdef USE_ENTITY_ICON
#define LOG_ENTITY_ICON(tag, prefix, obj) log_entity_icon(tag, prefix, obj)
void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj);
#else
#define LOG_ENTITY_ICON(tag, prefix, obj)
#endif
/// Log entity device class if set (for use in dump_config)
#define LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj) log_entity_device_class(tag, prefix, obj)
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj);

View File

@@ -81,19 +81,6 @@ class StringRef {
operator std::string() const { return str(); }
/// Compare with a null-terminated C string (compatible with std::string::compare)
int compare(const char *s) const {
size_t s_len = std::strlen(s);
int result = std::memcmp(base_, s, std::min(len_, s_len));
if (result != 0)
return result;
if (len_ < s_len)
return -1;
if (len_ > s_len)
return 1;
return 0;
}
/// Find first occurrence of substring, returns std::string::npos if not found.
/// Note: Requires the underlying string to be null-terminated.
size_type find(const char *s, size_type pos = 0) const {

View File

@@ -24,14 +24,11 @@ class RegistryEntry:
fun: Callable[..., Any],
type_id: "MockObjClass",
schema: "Schema",
*,
deferred: bool = False,
):
self.name = name
self.fun = fun
self.type_id = type_id
self.raw_schema = schema
self.deferred = deferred
@property
def coroutine_fun(self):
@@ -52,16 +49,9 @@ class Registry(dict[str, RegistryEntry]):
self.base_schema = base_schema or {}
self.type_id_key = type_id_key
def register(
self,
name: str,
type_id: "MockObjClass",
schema: "Schema",
*,
deferred: bool = False,
):
def register(self, name: str, type_id: "MockObjClass", schema: "Schema"):
def decorator(fun: Callable[..., Any]):
self[name] = RegistryEntry(name, fun, type_id, schema, deferred=deferred)
self[name] = RegistryEntry(name, fun, type_id, schema)
return fun
return decorator

View File

@@ -2020,8 +2020,6 @@ def build_message_type(
# Collect fixed_vector fields for custom decode generation
fixed_vector_fields = []
# Collect fields with (null_terminate) = true option
null_terminate_fields = []
for field in desc.field:
# Skip deprecated fields completely
@@ -2064,10 +2062,6 @@ def build_message_type(
ti = create_field_type_info(field, needs_decode, needs_encode)
# Collect fields with (null_terminate) = true for post-decode null-termination
if needs_decode and get_field_opt(field, pb.null_terminate, False):
null_terminate_fields.append(ti.field_name)
# Skip field declarations for fields that are in the base class
# but include their encode/decode logic
if field.name not in common_field_names:
@@ -2174,8 +2168,8 @@ def build_message_type(
prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;"
protected_content.insert(0, prot)
# Generate custom decode() override for messages with FixedVector or null_terminate fields
if fixed_vector_fields or null_terminate_fields:
# Generate custom decode() override for messages with FixedVector fields
if fixed_vector_fields:
# Generate the decode() implementation in cpp
o = f"void {desc.name}::decode(const uint8_t *buffer, size_t length) {{\n"
# Count and init each FixedVector field
@@ -2184,13 +2178,6 @@ def build_message_type(
o += f" this->{field_name}.init(count_{field_name});\n"
# Call parent decode to populate the fields
o += " ProtoDecodableMessage::decode(buffer, length);\n"
# Null-terminate fields marked with (null_terminate) = true in-place.
# Safe: decode is complete, byte after string was already parsed (next field tag)
# or is the +1 reserved byte at end of rx_buf_.
for field_name in null_terminate_fields:
o += f" if (!this->{field_name}.empty()) {{\n"
o += f" const_cast<char *>(this->{field_name}.c_str())[this->{field_name}.size()] = '\\0';\n"
o += " }\n"
o += "}\n"
cpp += o
# Generate the decode() declaration in header (public method)