From f467c79a20ee14eb30035c9faa49a97b049ac26d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 11 Jun 2025 23:16:56 +1200 Subject: [PATCH 001/293] Bump version to 2025.7.0-dev --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index f807ba5c4e..03d432b924 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.6.0-dev +PROJECT_NUMBER = 2025.7.0-dev # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 49dc663a9f..c01f30c3ff 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.6.0-dev" +__version__ = "2025.7.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From a59e1c7011142e3d44b11efe688e9685f6b29eae Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Wed, 11 Jun 2025 20:06:41 +0200 Subject: [PATCH 002/293] [core/pins] improve pins types (#8848) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/mcp23016/__init__.py | 2 +- esphome/components/mcp23xxx_base/__init__.py | 2 +- esphome/components/pcf8574/__init__.py | 2 +- esphome/components/sn74hc595/__init__.py | 2 +- esphome/components/tca9555/__init__.py | 2 +- esphome/pins.py | 18 +++++++++++------- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/esphome/components/mcp23016/__init__.py b/esphome/components/mcp23016/__init__.py index e15c643349..3333e46c97 100644 --- a/esphome/components/mcp23016/__init__.py +++ b/esphome/components/mcp23016/__init__.py @@ -50,7 +50,7 @@ MCP23016_PIN_SCHEMA = pins.gpio_base_schema( cv.int_range(min=0, max=15), modes=[CONF_INPUT, CONF_OUTPUT], mode_validator=validate_mode, - invertable=True, + invertible=True, ).extend( { cv.Required(CONF_MCP23016): cv.use_id(MCP23016), diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index c0e44d72de..8cf0ebcd44 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -60,7 +60,7 @@ MCP23XXX_PIN_SCHEMA = pins.gpio_base_schema( cv.int_range(min=0, max=15), modes=[CONF_INPUT, CONF_OUTPUT, CONF_PULLUP], mode_validator=validate_mode, - invertable=True, + invertible=True, ).extend( { cv.Required(CONF_MCP23XXX): cv.use_id(MCP23XXXBase), diff --git a/esphome/components/pcf8574/__init__.py b/esphome/components/pcf8574/__init__.py index 64bef86443..ff7c314bcd 100644 --- a/esphome/components/pcf8574/__init__.py +++ b/esphome/components/pcf8574/__init__.py @@ -53,7 +53,7 @@ PCF8574_PIN_SCHEMA = pins.gpio_base_schema( cv.int_range(min=0, max=17), modes=[CONF_INPUT, CONF_OUTPUT], mode_validator=validate_mode, - invertable=True, + invertible=True, ).extend( { cv.Required(CONF_PCF8574): cv.use_id(PCF8574Component), diff --git a/esphome/components/sn74hc595/__init__.py b/esphome/components/sn74hc595/__init__.py index db18b00cd1..26e5c03802 100644 --- a/esphome/components/sn74hc595/__init__.py +++ b/esphome/components/sn74hc595/__init__.py @@ -95,7 +95,7 @@ SN74HC595_PIN_SCHEMA = pins.gpio_base_schema( cv.int_range(min=0, max=2047), modes=[CONF_OUTPUT], mode_validator=_validate_output_mode, - invertable=True, + invertible=True, ).extend( { cv.Required(CONF_SN74HC595): cv.use_id(SN74HC595Component), diff --git a/esphome/components/tca9555/__init__.py b/esphome/components/tca9555/__init__.py index db0451d4e6..f42e0fe398 100644 --- a/esphome/components/tca9555/__init__.py +++ b/esphome/components/tca9555/__init__.py @@ -53,7 +53,7 @@ TCA9555_PIN_SCHEMA = pins.gpio_base_schema( cv.int_range(min=0, max=15), modes=[CONF_INPUT, CONF_OUTPUT], mode_validator=validate_mode, - invertable=True, + invertible=True, ).extend( { cv.Required(CONF_TCA9555): cv.use_id(TCA9555Component), diff --git a/esphome/pins.py b/esphome/pins.py index 724cd25d82..0dfd5a245b 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -1,5 +1,8 @@ +from collections.abc import Callable from functools import reduce +from logging import Logger import operator +from typing import Any import esphome.config_validation as cv from esphome.const import ( @@ -15,6 +18,7 @@ from esphome.const import ( CONF_PULLUP, ) from esphome.core import CORE +from esphome.cpp_generator import MockObjClass class PinRegistry(dict): @@ -262,7 +266,7 @@ internal_gpio_input_pullup_pin_number = _internal_number_creator( ) -def check_strapping_pin(conf, strapping_pin_list, logger): +def check_strapping_pin(conf, strapping_pin_list: set[int], logger: Logger): num = conf[CONF_NUMBER] if num in strapping_pin_list and not conf.get(CONF_IGNORE_STRAPPING_WARNING): logger.warning( @@ -291,11 +295,11 @@ def gpio_validate_modes(value): def gpio_base_schema( - pin_type, - number_validator, + pin_type: MockObjClass, + number_validator: Callable[[Any], Any], modes=GPIO_STANDARD_MODES, - mode_validator=gpio_validate_modes, - invertable=True, + mode_validator: Callable[[Any], Any] = gpio_validate_modes, + invertible: bool = True, ): """ Generate a base gpio pin schema @@ -303,7 +307,7 @@ def gpio_base_schema( :param number_validator: A validator for the pin number :param modes: The available modes, default is all standard modes :param mode_validator: A validator function for the pin mode - :param invertable: If the pin supports hardware inversion + :param invertible: If the pin supports hardware inversion :return: A schema for the pin """ mode_default = len(modes) == 1 @@ -328,7 +332,7 @@ def gpio_base_schema( } ) - if invertable: + if invertible: return schema.extend({cv.Optional(CONF_INVERTED, default=False): cv.boolean}) return schema From 0228379a2e5ed30030f4a36c45a857a1f6eb5432 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Jun 2025 16:17:47 -0500 Subject: [PATCH 003/293] Fix dashboard logging being escaped before parser (#9054) --- esphome/components/api/client.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 4edcc90f4a..20136ef7b8 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -46,12 +46,10 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None: time_ = datetime.now() message: bytes = msg.message text = message.decode("utf8", "backslashreplace") - if dashboard: - text = text.replace("\033", "\\033") for parsed_msg in parse_log_message( text, f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]" ): - print(parsed_msg) + print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg) stop = await async_run(cli, on_log, name=name) try: From 261b561bb234c33209e3bd640fa4d1081f37e576 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 12 Jun 2025 09:16:20 +1000 Subject: [PATCH 004/293] [binary_sensor] Add action to invalidate state and pass to HA (#8961) Co-authored-by: J. Nick Koston --- esphome/components/api/api_server.cpp | 2 +- esphome/components/api/api_server.h | 2 +- esphome/components/binary_sensor/__init__.py | 66 ++++++++++++++++++- esphome/components/binary_sensor/automation.h | 20 +++++- .../binary_sensor/binary_sensor.cpp | 44 ++++--------- .../components/binary_sensor/binary_sensor.h | 36 ++++------ esphome/components/binary_sensor/filter.cpp | 51 +++++++------- esphome/components/binary_sensor/filter.h | 20 +++--- esphome/components/const/__init__.py | 1 + .../binary_sensor/template_binary_sensor.cpp | 10 +-- esphome/components/web_server/web_server.cpp | 2 +- esphome/components/web_server/web_server.h | 2 +- esphome/core/controller.cpp | 6 +- esphome/core/controller.h | 2 +- esphome/core/entity_base.h | 61 ++++++++++++++++- esphome/core/log.h | 2 + esphome/core/optional.h | 5 ++ tests/components/binary_sensor/common.yaml | 15 +++++ .../binary_sensor/test.bk72xx-ard.yaml | 2 + .../binary_sensor/test.esp32-ard.yaml | 2 + .../binary_sensor/test.esp32-c3-ard.yaml | 2 + .../binary_sensor/test.esp32-c3-idf.yaml | 2 + .../binary_sensor/test.esp32-idf.yaml | 2 + .../binary_sensor/test.esp32-s3-idf.yaml | 2 + .../binary_sensor/test.esp8266-ard.yaml | 2 + .../binary_sensor/test.rp2040-ard.yaml | 2 + tests/components/lvgl/common.yaml | 2 +- 27 files changed, 250 insertions(+), 115 deletions(-) create mode 100644 tests/components/binary_sensor/common.yaml create mode 100644 tests/components/binary_sensor/test.bk72xx-ard.yaml create mode 100644 tests/components/binary_sensor/test.esp32-ard.yaml create mode 100644 tests/components/binary_sensor/test.esp32-c3-ard.yaml create mode 100644 tests/components/binary_sensor/test.esp32-c3-idf.yaml create mode 100644 tests/components/binary_sensor/test.esp32-idf.yaml create mode 100644 tests/components/binary_sensor/test.esp32-s3-idf.yaml create mode 100644 tests/components/binary_sensor/test.esp8266-ard.yaml create mode 100644 tests/components/binary_sensor/test.rp2040-ard.yaml diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 17c83c54f1..6852afe937 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -227,7 +227,7 @@ bool APIServer::check_password(const std::string &password) const { void APIServer::handle_disconnect(APIConnection *conn) {} #ifdef USE_BINARY_SENSOR -void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) { +void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { if (obj->is_internal()) return; for (auto &c : this->clients_) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 71e470d4f8..971c192e4b 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -54,7 +54,7 @@ class APIServer : public Component, public Controller { void handle_disconnect(APIConnection *conn); #ifdef USE_BINARY_SENSOR - void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override; + void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override; #endif #ifdef USE_COVER void on_cover_update(cover::Cover *obj) override; diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index ec1c4e8a0c..bc26c09622 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -1,7 +1,10 @@ +from logging import getLogger + from esphome import automation, core from esphome.automation import Condition, maybe_simple_id import esphome.codegen as cg from esphome.components import mqtt, web_server +from esphome.components.const import CONF_ON_STATE_CHANGE import esphome.config_validation as cv from esphome.const import ( CONF_DELAY, @@ -98,6 +101,7 @@ IS_PLATFORM_COMPONENT = True CONF_TIME_OFF = "time_off" CONF_TIME_ON = "time_on" +CONF_TRIGGER_ON_INITIAL_STATE = "trigger_on_initial_state" DEFAULT_DELAY = "1s" DEFAULT_TIME_OFF = "100ms" @@ -127,9 +131,17 @@ MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent") StateTrigger = binary_sensor_ns.class_( "StateTrigger", automation.Trigger.template(bool) ) +StateChangeTrigger = binary_sensor_ns.class_( + "StateChangeTrigger", + automation.Trigger.template(cg.optional.template(bool), cg.optional.template(bool)), +) + BinarySensorPublishAction = binary_sensor_ns.class_( "BinarySensorPublishAction", automation.Action ) +BinarySensorInvalidateAction = binary_sensor_ns.class_( + "BinarySensorInvalidateAction", automation.Action +) # Condition BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Condition) @@ -144,6 +156,8 @@ AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Compon LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component) +_LOGGER = getLogger(__name__) + FILTER_REGISTRY = Registry() validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) @@ -386,6 +400,14 @@ def validate_click_timing(value): return value +def validate_publish_initial_state(value): + value = cv.boolean(value) + _LOGGER.warning( + "The 'publish_initial_state' option has been replaced by 'trigger_on_initial_state' and will be removed in a future release" + ) + return value + + _BINARY_SENSOR_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMPONENT_SCHEMA) @@ -395,7 +417,12 @@ _BINARY_SENSOR_SCHEMA = ( cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( mqtt.MQTTBinarySensorComponent ), - cv.Optional(CONF_PUBLISH_INITIAL_STATE): cv.boolean, + cv.Exclusive( + CONF_PUBLISH_INITIAL_STATE, CONF_TRIGGER_ON_INITIAL_STATE + ): validate_publish_initial_state, + cv.Exclusive( + CONF_TRIGGER_ON_INITIAL_STATE, CONF_TRIGGER_ON_INITIAL_STATE + ): cv.boolean, cv.Optional(CONF_DEVICE_CLASS): validate_device_class, cv.Optional(CONF_FILTERS): validate_filters, cv.Optional(CONF_ON_PRESS): automation.validate_automation( @@ -454,6 +481,11 @@ _BINARY_SENSOR_SCHEMA = ( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), } ), + cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateChangeTrigger), + } + ), } ) ) @@ -493,8 +525,10 @@ async def setup_binary_sensor_core_(var, config): if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: cg.add(var.set_device_class(device_class)) - if publish_initial_state := config.get(CONF_PUBLISH_INITIAL_STATE): - cg.add(var.set_publish_initial_state(publish_initial_state)) + trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get( + CONF_PUBLISH_INITIAL_STATE, False + ) + cg.add(var.set_trigger_on_initial_state(trigger)) if inverted := config.get(CONF_INVERTED): cg.add(var.set_inverted(inverted)) if filters_config := config.get(CONF_FILTERS): @@ -542,6 +576,17 @@ async def setup_binary_sensor_core_(var, config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(bool, "x")], conf) + for conf in config.get(CONF_ON_STATE_CHANGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, + [ + (cg.optional.template(bool), "x_previous"), + (cg.optional.template(bool), "x"), + ], + conf, + ) + if mqtt_id := config.get(CONF_MQTT_ID): mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) @@ -591,3 +636,18 @@ async def binary_sensor_is_off_to_code(config, condition_id, template_arg, args) async def to_code(config): cg.add_define("USE_BINARY_SENSOR") cg.add_global(binary_sensor_ns.using) + + +@automation.register_action( + "binary_sensor.invalidate_state", + BinarySensorInvalidateAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(BinarySensor), + }, + key=CONF_ID, + ), +) +async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index 12b07a05e3..b46436dc41 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -96,7 +96,7 @@ class MultiClickTrigger : public Trigger<>, public Component { : parent_(parent), timing_(std::move(timing)) {} void setup() override { - this->last_state_ = this->parent_->state; + this->last_state_ = this->parent_->get_state_default(false); auto f = std::bind(&MultiClickTrigger::on_state_, this, std::placeholders::_1); this->parent_->add_on_state_callback(f); } @@ -130,6 +130,14 @@ class StateTrigger : public Trigger { } }; +class StateChangeTrigger : public Trigger, optional > { + public: + explicit StateChangeTrigger(BinarySensor *parent) { + parent->add_full_state_callback( + [this](optional old_state, optional state) { this->trigger(old_state, state); }); + } +}; + template class BinarySensorCondition : public Condition { public: BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {} @@ -154,5 +162,15 @@ template class BinarySensorPublishAction : public Action BinarySensor *sensor_; }; +template class BinarySensorInvalidateAction : public Action { + public: + explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {} + + void play(Ts... x) override { this->sensor_->invalidate_state(); } + + protected: + BinarySensor *sensor_; +}; + } // namespace binary_sensor } // namespace esphome diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 20604a0b7e..02b83af552 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -7,42 +7,25 @@ namespace binary_sensor { static const char *const TAG = "binary_sensor"; -void BinarySensor::add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); -} - -void BinarySensor::publish_state(bool state) { - if (!this->publish_dedup_.next(state)) - return; +void BinarySensor::publish_state(bool new_state) { if (this->filter_list_ == nullptr) { - this->send_state_internal(state, false); + this->send_state_internal(new_state); } else { - this->filter_list_->input(state, false); + this->filter_list_->input(new_state); } } -void BinarySensor::publish_initial_state(bool state) { - if (!this->publish_dedup_.next(state)) - return; - if (this->filter_list_ == nullptr) { - this->send_state_internal(state, true); - } else { - this->filter_list_->input(state, true); +void BinarySensor::publish_initial_state(bool new_state) { + this->invalidate_state(); + this->publish_state(new_state); +} +void BinarySensor::send_state_internal(bool new_state) { + // copy the new state to the visible property for backwards compatibility, before any callbacks + this->state = new_state; + // Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed + if (this->set_state_(new_state)) { + ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state)); } } -void BinarySensor::send_state_internal(bool state, bool is_initial) { - if (is_initial) { - ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state)); - } else { - ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), ONOFF(state)); - } - this->has_state_ = true; - this->state = state; - if (!is_initial || this->publish_initial_state_) { - this->state_callback_.call(state); - } -} - -BinarySensor::BinarySensor() : state(false) {} void BinarySensor::add_filter(Filter *filter) { filter->parent_ = this; @@ -60,7 +43,6 @@ void BinarySensor::add_filters(const std::vector &filters) { this->add_filter(filter); } } -bool BinarySensor::has_state() const { return this->has_state_; } bool BinarySensor::is_status_binary_sensor() const { return false; } } // namespace binary_sensor diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 57cae9e2f5..d61be7a49b 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -1,6 +1,5 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/components/binary_sensor/filter.h" @@ -34,52 +33,39 @@ namespace binary_sensor { * The sub classes should notify the front-end of new states via the publish_state() method which * handles inverted inputs for you. */ -class BinarySensor : public EntityBase, public EntityBase_DeviceClass { +class BinarySensor : public StatefulEntityBase, public EntityBase_DeviceClass { public: - explicit BinarySensor(); - - /** Add a callback to be notified of state changes. - * - * @param callback The void(bool) callback. - */ - void add_on_state_callback(std::function &&callback); + explicit BinarySensor(){}; /** Publish a new state to the front-end. * - * @param state The new state. + * @param new_state The new state. */ - void publish_state(bool state); + void publish_state(bool new_state); /** Publish the initial state, this will not make the callback manager send callbacks * and is meant only for the initial state on boot. * - * @param state The new state. + * @param new_state The new state. */ - void publish_initial_state(bool state); - - /// The current reported state of the binary sensor. - bool state{false}; + void publish_initial_state(bool new_state); void add_filter(Filter *filter); void add_filters(const std::vector &filters); - void set_publish_initial_state(bool publish_initial_state) { this->publish_initial_state_ = publish_initial_state; } - // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) - void send_state_internal(bool state, bool is_initial); + void send_state_internal(bool new_state); /// Return whether this binary sensor has outputted a state. - virtual bool has_state() const; - virtual bool is_status_binary_sensor() const; + // For backward compatibility, provide an accessible property + + bool state{}; + protected: - CallbackManager state_callback_{}; Filter *filter_list_{nullptr}; - bool has_state_{false}; - bool publish_initial_state_{false}; - Deduplicator publish_dedup_; }; class BinarySensorInitiallyOff : public BinarySensor { diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 8f94b108ac..41d0553b35 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -9,37 +9,36 @@ namespace binary_sensor { static const char *const TAG = "sensor.filter"; -void Filter::output(bool value, bool is_initial) { +void Filter::output(bool value) { + if (this->next_ == nullptr) { + this->parent_->send_state_internal(value); + } else { + this->next_->input(value); + } +} +void Filter::input(bool value) { if (!this->dedup_.next(value)) return; - - if (this->next_ == nullptr) { - this->parent_->send_state_internal(value, is_initial); - } else { - this->next_->input(value, is_initial); - } -} -void Filter::input(bool value, bool is_initial) { - auto b = this->new_value(value, is_initial); + auto b = this->new_value(value); if (b.has_value()) { - this->output(*b, is_initial); + this->output(*b); } } -optional DelayedOnOffFilter::new_value(bool value, bool is_initial) { +optional DelayedOnOffFilter::new_value(bool value) { if (value) { - this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); }); + this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); }); } else { - this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); }); + this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); }); } return {}; } float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -optional DelayedOnFilter::new_value(bool value, bool is_initial) { +optional DelayedOnFilter::new_value(bool value) { if (value) { - this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); }); + this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); }); return {}; } else { this->cancel_timeout("ON"); @@ -49,9 +48,9 @@ optional DelayedOnFilter::new_value(bool value, bool is_initial) { float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -optional DelayedOffFilter::new_value(bool value, bool is_initial) { +optional DelayedOffFilter::new_value(bool value) { if (!value) { - this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); }); + this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); }); return {}; } else { this->cancel_timeout("OFF"); @@ -61,11 +60,11 @@ optional DelayedOffFilter::new_value(bool value, bool is_initial) { float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -optional InvertFilter::new_value(bool value, bool is_initial) { return !value; } +optional InvertFilter::new_value(bool value) { return !value; } AutorepeatFilter::AutorepeatFilter(std::vector timings) : timings_(std::move(timings)) {} -optional AutorepeatFilter::new_value(bool value, bool is_initial) { +optional AutorepeatFilter::new_value(bool value) { if (value) { // Ignore if already running if (this->active_timing_ != 0) @@ -101,7 +100,7 @@ void AutorepeatFilter::next_timing_() { void AutorepeatFilter::next_value_(bool val) { const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2]; - this->output(val, false); // This is at least the second one so not initial + this->output(val); // This is at least the second one so not initial this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); }); } @@ -109,18 +108,18 @@ float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARD LambdaFilter::LambdaFilter(std::function(bool)> f) : f_(std::move(f)) {} -optional LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); } +optional LambdaFilter::new_value(bool value) { return this->f_(value); } -optional SettleFilter::new_value(bool value, bool is_initial) { +optional SettleFilter::new_value(bool value) { if (!this->steady_) { - this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() { + this->set_timeout("SETTLE", this->delay_.value(), [this, value]() { this->steady_ = true; - this->output(value, is_initial); + this->output(value); }); return {}; } else { this->steady_ = false; - this->output(value, is_initial); + this->output(value); this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; }); return value; } diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index f7342db2fb..65838da49d 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -14,11 +14,11 @@ class BinarySensor; class Filter { public: - virtual optional new_value(bool value, bool is_initial) = 0; + virtual optional new_value(bool value) = 0; - void input(bool value, bool is_initial); + void input(bool value); - void output(bool value, bool is_initial); + void output(bool value); protected: friend BinarySensor; @@ -30,7 +30,7 @@ class Filter { class DelayedOnOffFilter : public Filter, public Component { public: - optional new_value(bool value, bool is_initial) override; + optional new_value(bool value) override; float get_setup_priority() const override; @@ -44,7 +44,7 @@ class DelayedOnOffFilter : public Filter, public Component { class DelayedOnFilter : public Filter, public Component { public: - optional new_value(bool value, bool is_initial) override; + optional new_value(bool value) override; float get_setup_priority() const override; @@ -56,7 +56,7 @@ class DelayedOnFilter : public Filter, public Component { class DelayedOffFilter : public Filter, public Component { public: - optional new_value(bool value, bool is_initial) override; + optional new_value(bool value) override; float get_setup_priority() const override; @@ -68,7 +68,7 @@ class DelayedOffFilter : public Filter, public Component { class InvertFilter : public Filter { public: - optional new_value(bool value, bool is_initial) override; + optional new_value(bool value) override; }; struct AutorepeatFilterTiming { @@ -86,7 +86,7 @@ class AutorepeatFilter : public Filter, public Component { public: explicit AutorepeatFilter(std::vector timings); - optional new_value(bool value, bool is_initial) override; + optional new_value(bool value) override; float get_setup_priority() const override; @@ -102,7 +102,7 @@ class LambdaFilter : public Filter { public: explicit LambdaFilter(std::function(bool)> f); - optional new_value(bool value, bool is_initial) override; + optional new_value(bool value) override; protected: std::function(bool)> f_; @@ -110,7 +110,7 @@ class LambdaFilter : public Filter { class SettleFilter : public Filter, public Component { public: - optional new_value(bool value, bool is_initial) override; + optional new_value(bool value) override; float get_setup_priority() const override; diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index a73849e67d..66a5fe5d81 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -3,4 +3,5 @@ CODEOWNERS = ["@esphome/core"] CONF_DRAW_ROUNDING = "draw_rounding" +CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index 5ce8894a8a..d1fb618695 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -6,16 +6,8 @@ namespace template_ { static const char *const TAG = "template.binary_sensor"; -void TemplateBinarySensor::setup() { - if (!this->publish_initial_state_) - return; +void TemplateBinarySensor::setup() { this->loop(); } - if (this->f_ != nullptr) { - this->publish_initial_state(this->f_().value_or(false)); - } else { - this->publish_initial_state(false); - } -} void TemplateBinarySensor::loop() { if (this->f_ == nullptr) return; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 870932d266..7ae30522f4 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -555,7 +555,7 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) #endif #ifdef USE_BINARY_SENSOR -void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) { +void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { if (this->events_.empty()) return; this->events_.deferrable_send_state(obj, "state", binary_sensor_state_json_generator); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index e4f044c50b..f4d6ad8e86 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -269,7 +269,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_BINARY_SENSOR - void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override; + void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override; /// Handle a binary sensor request under '/binary_sensor/'. void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp index d6d98a4316..f7ff5a9734 100644 --- a/esphome/core/controller.cpp +++ b/esphome/core/controller.cpp @@ -7,8 +7,10 @@ namespace esphome { void Controller::setup_controller(bool include_internal) { #ifdef USE_BINARY_SENSOR for (auto *obj : App.get_binary_sensors()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](bool state) { this->on_binary_sensor_update(obj, state); }); + if (include_internal || !obj->is_internal()) { + obj->add_full_state_callback( + [this, obj](optional previous, optional state) { this->on_binary_sensor_update(obj); }); + } } #endif #ifdef USE_FAN diff --git a/esphome/core/controller.h b/esphome/core/controller.h index 39e0b2ba26..1a5b9ea6b4 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -71,7 +71,7 @@ class Controller { public: void setup_controller(bool include_internal = false); #ifdef USE_BINARY_SENSOR - virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state){}; + virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj){}; #endif #ifdef USE_FAN virtual void on_fan_update(fan::Fan *obj){}; diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 4ca21f9ee5..a2e1d4adbc 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -3,6 +3,8 @@ #include #include #include "string_ref.h" +#include "helpers.h" +#include "log.h" namespace esphome { @@ -29,7 +31,7 @@ class EntityBase { // Get the unique Object ID of this Entity uint32_t get_object_id_hash(); - // Get/set whether this Entity should be hidden from outside of ESPHome + // Get/set whether this Entity should be hidden outside ESPHome bool is_internal() const; void set_internal(bool internal); @@ -56,11 +58,12 @@ class EntityBase { StringRef name_; const char *object_id_c_str_{nullptr}; const char *icon_c_str_{nullptr}; - uint32_t object_id_hash_; + uint32_t object_id_hash_{}; bool has_own_name_{false}; bool internal_{false}; bool disabled_by_default_{false}; EntityCategory entity_category_{ENTITY_CATEGORY_NONE}; + bool has_state_{}; }; class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) @@ -85,4 +88,58 @@ class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) const char *unit_of_measurement_{nullptr}; ///< Unit of measurement override }; +/** + * An entity that has a state. + * @tparam T The type of the state + */ +template class StatefulEntityBase : public EntityBase { + public: + virtual bool has_state() const { return this->state_.has_value(); } + virtual const T &get_state() const { return this->state_.value(); } + virtual T get_state_default(T default_value) const { return this->state_.value_or(default_value); } + void invalidate_state() { this->set_state_({}); } + + void add_full_state_callback(std::function previous, optional current)> &&callback) { + if (this->full_state_callbacks_ == nullptr) + this->full_state_callbacks_ = new CallbackManager previous, optional current)>(); // NOLINT + this->full_state_callbacks_->add(std::move(callback)); + } + void add_on_state_callback(std::function &&callback) { + if (this->state_callbacks_ == nullptr) + this->state_callbacks_ = new CallbackManager(); // NOLINT + this->state_callbacks_->add(std::move(callback)); + } + + void set_trigger_on_initial_state(bool trigger_on_initial_state) { + this->trigger_on_initial_state_ = trigger_on_initial_state; + } + + protected: + optional state_{}; + /** + * Set a new state for this entity. This will trigger callbacks only if the new state is different from the previous. + * + * @param state The new state. + * @return True if the state was changed, false if it was the same as before. + */ + bool set_state_(const optional &state) { + if (this->state_ != state) { + // call the full state callbacks with the previous and new state + if (this->full_state_callbacks_ != nullptr) + this->full_state_callbacks_->call(this->state_, state); + // trigger legacy callbacks only if the new state is valid and either the trigger on initial state is enabled or + // the previous state was valid + auto had_state = this->has_state(); + this->state_ = state; + if (this->state_callbacks_ != nullptr && state.has_value() && (this->trigger_on_initial_state_ || had_state)) + this->state_callbacks_->call(state.value()); + return true; + } + return false; + } + bool trigger_on_initial_state_{true}; + // callbacks with full state and previous state + CallbackManager previous, optional current)> *full_state_callbacks_{}; + CallbackManager *state_callbacks_{}; +}; } // namespace esphome diff --git a/esphome/core/log.h b/esphome/core/log.h index adf72e4bac..cade6a74c1 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -165,6 +165,8 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #define YESNO(b) ((b) ? "YES" : "NO") #define ONOFF(b) ((b) ? "ON" : "OFF") #define TRUEFALSE(b) ((b) ? "TRUE" : "FALSE") +// for use with optional values +#define ONOFFMAYBE(b) (((b).has_value()) ? ONOFF((b).value()) : "UNKNOWN") // Helper class that identifies strings that may be stored in flash storage (similar to Arduino's __FlashStringHelper) struct LogString; diff --git a/esphome/core/optional.h b/esphome/core/optional.h index 591bc7aa68..7f9db7817d 100644 --- a/esphome/core/optional.h +++ b/esphome/core/optional.h @@ -52,6 +52,11 @@ template class optional { // NOLINT reset(); return *this; } + bool operator==(optional const &rhs) const { + if (has_value() && rhs.has_value()) + return value() == rhs.value(); + return !has_value() && !rhs.has_value(); + } template optional &operator=(optional const &other) { has_value_ = other.has_value(); diff --git a/tests/components/binary_sensor/common.yaml b/tests/components/binary_sensor/common.yaml new file mode 100644 index 0000000000..148b7d2405 --- /dev/null +++ b/tests/components/binary_sensor/common.yaml @@ -0,0 +1,15 @@ +binary_sensor: + - platform: template + trigger_on_initial_state: true + id: some_binary_sensor + name: "Random binary" + lambda: return (random_uint32() & 1) == 0; + on_state_change: + then: + - logger.log: + format: "Old state was %s" + args: ['x_previous.has_value() ? ONOFF(x_previous) : "Unknown"'] + - logger.log: + format: "New state is %s" + args: ['x.has_value() ? ONOFF(x) : "Unknown"'] + - binary_sensor.invalidate_state: some_binary_sensor diff --git a/tests/components/binary_sensor/test.bk72xx-ard.yaml b/tests/components/binary_sensor/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/binary_sensor/test.bk72xx-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/binary_sensor/test.esp32-ard.yaml b/tests/components/binary_sensor/test.esp32-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/binary_sensor/test.esp32-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/binary_sensor/test.esp32-c3-ard.yaml b/tests/components/binary_sensor/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/binary_sensor/test.esp32-c3-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/binary_sensor/test.esp32-c3-idf.yaml b/tests/components/binary_sensor/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/binary_sensor/test.esp32-c3-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/binary_sensor/test.esp32-idf.yaml b/tests/components/binary_sensor/test.esp32-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/binary_sensor/test.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/binary_sensor/test.esp32-s3-idf.yaml b/tests/components/binary_sensor/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/binary_sensor/test.esp32-s3-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/binary_sensor/test.esp8266-ard.yaml b/tests/components/binary_sensor/test.esp8266-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/binary_sensor/test.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/binary_sensor/test.rp2040-ard.yaml b/tests/components/binary_sensor/test.rp2040-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/binary_sensor/test.rp2040-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index 174df56749..59602414a7 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -63,7 +63,7 @@ binary_sensor: id: lvgl_pressbutton name: Pressbutton widget: spin_up - publish_initial_state: true + trigger_on_initial_state: true - platform: lvgl name: ButtonMatrix button widget: button_a From dac738a916689808aea642c9662b8fcda707d771 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Jun 2025 22:27:10 -0500 Subject: [PATCH 005/293] Always perform select() when loop duration exceeds interval (#9058) --- esphome/core/application.cpp | 18 ++++++++++++------ esphome/core/application.h | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 75a7052c63..87e6f33e04 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -117,7 +117,9 @@ void Application::loop() { // Use the last component's end time instead of calling millis() again auto elapsed = last_op_end_time - this->last_loop_; if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { - yield(); + // Even if we overran the loop interval, we still need to select() + // to know if any sockets have data ready + this->yield_with_select_(0); } else { uint32_t delay_time = this->loop_interval_ - elapsed; uint32_t next_schedule = this->scheduler.next_schedule_in().value_or(delay_time); @@ -126,7 +128,7 @@ void Application::loop() { next_schedule = std::max(next_schedule, delay_time / 2); delay_time = std::min(next_schedule, delay_time); - this->delay_with_select_(delay_time); + this->yield_with_select_(delay_time); } this->last_loop_ = last_op_end_time; @@ -215,7 +217,7 @@ void Application::teardown_components(uint32_t timeout_ms) { // Give some time for I/O operations if components are still pending if (!pending_components.empty()) { - this->delay_with_select_(1); + this->yield_with_select_(1); } // Update time for next iteration @@ -293,8 +295,6 @@ bool Application::is_socket_ready(int fd) const { // This function is thread-safe for reading the result of select() // However, it should only be called after select() has been executed in the main loop // The read_fds_ is only modified by select() in the main loop - if (HighFrequencyLoopRequester::is_high_frequency()) - return true; // fd sets via select are not updated in high frequency looping - so force true fallback behavior if (fd < 0 || fd >= FD_SETSIZE) return false; @@ -302,7 +302,9 @@ bool Application::is_socket_ready(int fd) const { } #endif -void Application::delay_with_select_(uint32_t delay_ms) { +void Application::yield_with_select_(uint32_t delay_ms) { + // Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run + // since select() with 0 timeout only polls without yielding. #ifdef USE_SOCKET_SELECT_SUPPORT if (!this->socket_fds_.empty()) { // Update fd_set if socket list has changed @@ -340,6 +342,10 @@ void Application::delay_with_select_(uint32_t delay_ms) { ESP_LOGW(TAG, "select() failed with errno %d", errno); delay(delay_ms); } + // When delay_ms is 0, we need to yield since select(0) doesn't yield + if (delay_ms == 0) { + yield(); + } } else { // No sockets registered, use regular delay delay(delay_ms); diff --git a/esphome/core/application.h b/esphome/core/application.h index d95f45e757..6c09b25590 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -575,7 +575,7 @@ class Application { void feed_wdt_arch_(); /// Perform a delay while also monitoring socket file descriptors for readiness - void delay_with_select_(uint32_t delay_ms); + void yield_with_select_(uint32_t delay_ms); std::vector components_{}; std::vector looping_components_{}; From 1f14c316a34d0bb172924e4ec06581bb075a03ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 18:16:37 -0500 Subject: [PATCH 006/293] Bump pytest-cov from 6.1.1 to 6.2.1 (#9063) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 689cd9e75e..43140063a3 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ pre-commit # Unit tests pytest==8.4.0 -pytest-cov==6.1.1 +pytest-cov==6.2.1 pytest-mock==3.14.1 pytest-asyncio==0.26.0 pytest-xdist==3.7.0 From 2a629cae9398d19633c2e60e0d6b08d42d13375f Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Fri, 13 Jun 2025 03:39:32 +0200 Subject: [PATCH 007/293] [nextion] Remove upload flags reset from success path to prevent TFT corruption (#9064) --- .../nextion/nextion_upload_arduino.cpp | 21 +++++++++++-------- .../components/nextion/nextion_upload_idf.cpp | 20 ++++++++++-------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index fcd665917c..6652e70172 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -337,23 +337,26 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { bool Nextion::upload_end_(bool successful) { ESP_LOGD(TAG, "TFT upload done: %s", YESNO(successful)); - this->is_updating_ = false; - this->ignore_is_setup_ = false; - - uint32_t baud_rate = this->parent_->get_baud_rate(); - if (baud_rate != this->original_baud_rate_) { - ESP_LOGD(TAG, "Baud back: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_); - this->parent_->set_baud_rate(this->original_baud_rate_); - this->parent_->load_settings(); - } if (successful) { ESP_LOGD(TAG, "Restart"); delay(1500); // NOLINT App.safe_reboot(); + delay(1500); // NOLINT } else { ESP_LOGE(TAG, "TFT upload failed"); + + this->is_updating_ = false; + this->ignore_is_setup_ = false; + + uint32_t baud_rate = this->parent_->get_baud_rate(); + if (baud_rate != this->original_baud_rate_) { + ESP_LOGD(TAG, "Baud back: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_); + this->parent_->set_baud_rate(this->original_baud_rate_); + this->parent_->load_settings(); + } } + return successful; } diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp index 8f54fbd8ac..fc98056bc3 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -337,15 +337,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { bool Nextion::upload_end_(bool successful) { ESP_LOGD(TAG, "TFT upload done: %s", YESNO(successful)); - this->is_updating_ = false; - this->ignore_is_setup_ = false; - - uint32_t baud_rate = this->parent_->get_baud_rate(); - if (baud_rate != this->original_baud_rate_) { - ESP_LOGD(TAG, "Baud back: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_); - this->parent_->set_baud_rate(this->original_baud_rate_); - this->parent_->load_settings(); - } if (successful) { ESP_LOGD(TAG, "Restart"); @@ -353,7 +344,18 @@ bool Nextion::upload_end_(bool successful) { App.safe_reboot(); } else { ESP_LOGE(TAG, "TFT upload failed"); + + this->is_updating_ = false; + this->ignore_is_setup_ = false; + + uint32_t baud_rate = this->parent_->get_baud_rate(); + if (baud_rate != this->original_baud_rate_) { + ESP_LOGD(TAG, "Baud back: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_); + this->parent_->set_baud_rate(this->original_baud_rate_); + this->parent_->load_settings(); + } } + return successful; } From 02469c2d4c7e8eeeee7f483a7ab0614d79b68bdb Mon Sep 17 00:00:00 2001 From: Nico B <17694+youknow0@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:17:38 +0200 Subject: [PATCH 008/293] ina219: powerdown the sensor on shutdown (#9053) --- esphome/components/ina219/ina219.cpp | 7 +++++++ esphome/components/ina219/ina219.h | 1 + 2 files changed, 8 insertions(+) diff --git a/esphome/components/ina219/ina219.cpp b/esphome/components/ina219/ina219.cpp index 8d5271fa84..52a3b1e067 100644 --- a/esphome/components/ina219/ina219.cpp +++ b/esphome/components/ina219/ina219.cpp @@ -129,6 +129,13 @@ void INA219Component::setup() { } } +void INA219Component::on_powerdown() { + // Mode = 0 -> power down + if (!this->write_byte_16(INA219_REGISTER_CONFIG, 0)) { + ESP_LOGE(TAG, "powerdown error"); + } +} + void INA219Component::dump_config() { ESP_LOGCONFIG(TAG, "INA219:"); LOG_I2C_DEVICE(this); diff --git a/esphome/components/ina219/ina219.h b/esphome/components/ina219/ina219.h index a6c0f2bc4c..115fa886e0 100644 --- a/esphome/components/ina219/ina219.h +++ b/esphome/components/ina219/ina219.h @@ -15,6 +15,7 @@ class INA219Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; float get_setup_priority() const override; void update() override; + void on_powerdown() override; void set_shunt_resistance_ohm(float shunt_resistance_ohm) { shunt_resistance_ohm_ = shunt_resistance_ohm; } void set_max_current_a(float max_current_a) { max_current_a_ = max_current_a; } From 666a3ee5e9f4516fd0032a7d06e233912fc5f182 Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Fri, 13 Jun 2025 14:31:00 -0400 Subject: [PATCH 009/293] Fix BYPASS_AUTO feature to work with or without an arming delay (#9051) --- .../template_alarm_control_panel.cpp | 23 +++++++++++-------- .../template_alarm_control_panel.h | 1 + 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index c550d60630..6f743a77ef 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -110,15 +110,7 @@ void TemplateAlarmControlPanel::loop() { delay = this->arming_night_time_; } if ((millis() - this->last_update_) > delay) { -#ifdef USE_BINARY_SENSOR - for (auto sensor_info : this->sensor_map_) { - // Check for sensors left on and set to bypass automatically and remove them from monitoring - if ((sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor_info.first->state)) { - ESP_LOGW(TAG, "%s is left on and will be automatically bypassed", sensor_info.first->get_name().c_str()); - this->bypassed_sensor_indicies_.push_back(sensor_info.second.store_index); - } - } -#endif + this->bypass_before_arming(); this->publish_state(this->desired_state_); } return; @@ -259,10 +251,23 @@ void TemplateAlarmControlPanel::arm_(optional code, alarm_control_p if (delay > 0) { this->publish_state(ACP_STATE_ARMING); } else { + this->bypass_before_arming(); this->publish_state(state); } } +void TemplateAlarmControlPanel::bypass_before_arming() { +#ifdef USE_BINARY_SENSOR + for (auto sensor_info : this->sensor_map_) { + // Check for sensors left on and set to bypass automatically and remove them from monitoring + if ((sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor_info.first->state)) { + ESP_LOGW(TAG, "'%s' is left on and will be automatically bypassed", sensor_info.first->get_name().c_str()); + this->bypassed_sensor_indicies_.push_back(sensor_info.second.store_index); + } + } +#endif +} + void TemplateAlarmControlPanel::control(const AlarmControlPanelCall &call) { if (call.get_state()) { if (call.get_state() == ACP_STATE_ARMED_AWAY) { diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index 0e3a5c77cf..c3b28e8efa 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -60,6 +60,7 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; } bool get_all_sensors_ready() { return this->sensors_ready_; }; void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } + void bypass_before_arming(); #ifdef USE_BINARY_SENSOR /** Add a binary_sensor to the alarm_panel. From d9da4cf24d0bd5a3656529a40c1b089f2311e8bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Jun 2025 16:10:33 -0500 Subject: [PATCH 010/293] Fix misleading comment in API (#9069) --- esphome/components/api/api_connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 93ba9248b4..d09b1107d2 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -260,7 +260,7 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes return 0; // Doesn't fit } - // Allocate exact buffer space needed (just the payload, not the overhead) + // Allocate buffer space - pass payload size, allocation functions add header/footer space ProtoWriteBuffer buffer = is_single ? conn->allocate_single_message_buffer(size) : conn->allocate_batch_message_buffer(size); From 731b7808cd6979e2fba02d5e79bd9861f86ba890 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 14 Jun 2025 11:08:07 +1200 Subject: [PATCH 011/293] [prometheus] Remove ``cv.only_with_arduino`` (#9061) --- esphome/components/prometheus/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/prometheus/__init__.py b/esphome/components/prometheus/__init__.py index b899fe7642..26a9e70f7c 100644 --- a/esphome/components/prometheus/__init__.py +++ b/esphome/components/prometheus/__init__.py @@ -31,7 +31,6 @@ CONFIG_SCHEMA = cv.Schema( } ), }, - cv.only_with_arduino, ).extend(cv.COMPONENT_SCHEMA) From 1a03b4949f765ee3986e7ab51921c48208231802 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 14 Jun 2025 11:17:06 +1200 Subject: [PATCH 012/293] [esp32] Dynamically set default framework based on variant (#9060) --- esphome/components/esp32/__init__.py | 33 +++++++++++++++++++++++++--- esphome/wizard.py | 16 -------------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b5d4c83f5e..157fd9db11 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -94,6 +94,13 @@ COMPILER_OPTIMIZATIONS = { "SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE", } +ARDUINO_ALLOWED_VARIANTS = [ + VARIANT_ESP32, + VARIANT_ESP32C3, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +] + def get_cpu_frequencies(*frequencies): return [str(x) + "MHZ" for x in frequencies] @@ -143,12 +150,17 @@ def set_core_data(config): CORE.data[KEY_ESP32][KEY_COMPONENTS] = {} elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" + if variant not in ARDUINO_ALLOWED_VARIANTS: + raise cv.Invalid( + f"ESPHome does not support using the Arduino framework for the {variant}. Please use the ESP-IDF framework instead.", + path=[CONF_FRAMEWORK, CONF_TYPE], + ) CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( config[CONF_FRAMEWORK][CONF_VERSION] ) CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD] - CORE.data[KEY_ESP32][KEY_VARIANT] = config[CONF_VARIANT] + CORE.data[KEY_ESP32][KEY_VARIANT] = variant CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {} return config @@ -618,6 +630,21 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( ) +def _set_default_framework(config): + if CONF_FRAMEWORK not in config: + config = config.copy() + + variant = config[CONF_VARIANT] + if variant in ARDUINO_ALLOWED_VARIANTS: + config[CONF_FRAMEWORK] = ARDUINO_FRAMEWORK_SCHEMA({}) + config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO + else: + config[CONF_FRAMEWORK] = ESP_IDF_FRAMEWORK_SCHEMA({}) + config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF + + return config + + FRAMEWORK_ESP_IDF = "esp-idf" FRAMEWORK_ARDUINO = "arduino" FRAMEWORK_SCHEMA = cv.typed_schema( @@ -627,7 +654,6 @@ FRAMEWORK_SCHEMA = cv.typed_schema( }, lower=True, space="-", - default_type=FRAMEWORK_ARDUINO, ) @@ -654,10 +680,11 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_PARTITIONS): cv.file_, cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), - cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, + cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA, } ), _detect_variant, + _set_default_framework, set_core_data, ) diff --git a/esphome/wizard.py b/esphome/wizard.py index ca987304e2..7b4d87be63 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -67,20 +67,6 @@ esp8266: """ ESP32_CONFIG = """ -esp32: - board: {board} - framework: - type: arduino -""" - -ESP32S2_CONFIG = """ -esp32: - board: {board} - framework: - type: esp-idf -""" - -ESP32C3_CONFIG = """ esp32: board: {board} framework: @@ -105,8 +91,6 @@ rtl87xx: HARDWARE_BASE_CONFIGS = { "ESP8266": ESP8266_CONFIG, "ESP32": ESP32_CONFIG, - "ESP32S2": ESP32S2_CONFIG, - "ESP32C3": ESP32C3_CONFIG, "RP2040": RP2040_CONFIG, "BK72XX": BK72XX_CONFIG, "RTL87XX": RTL87XX_CONFIG, From 1c488d375fe8ebedd47745864bbc8bd44757069b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Jun 2025 18:40:18 -0500 Subject: [PATCH 013/293] Bump pytest-asyncio from 0.26.0 to 1.0.0 (#9067) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 43140063a3..2c2549c64b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ pre-commit pytest==8.4.0 pytest-cov==6.2.1 pytest-mock==3.14.1 -pytest-asyncio==0.26.0 +pytest-asyncio==1.0.0 pytest-xdist==3.7.0 asyncmock==0.4.2 hypothesis==6.92.1 From 92ea697119ff5fac5259eec25b40419df1d9939f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Jun 2025 08:19:41 -0500 Subject: [PATCH 014/293] Fix `captive_portal` loading entire `web_server` (#9066) --- esphome/components/web_server_idf/__init__.py | 2 -- esphome/components/web_server_idf/web_server_idf.cpp | 8 ++++++-- esphome/components/web_server_idf/web_server_idf.h | 7 +++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server_idf/__init__.py b/esphome/components/web_server_idf/__init__.py index 73c51f8cb5..506e1c5c13 100644 --- a/esphome/components/web_server_idf/__init__.py +++ b/esphome/components/web_server_idf/__init__.py @@ -8,8 +8,6 @@ CONFIG_SCHEMA = cv.All( cv.only_with_esp_idf, ) -AUTO_LOAD = ["web_server"] - async def to_code(config): # Increase the maximum supported size of headers section in HTTP request packet to be processed by the server diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 6bfc49c675..90fdf720cd 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -9,10 +9,12 @@ #include "utils.h" +#include "web_server_idf.h" + +#ifdef USE_WEBSERVER #include "esphome/components/web_server/web_server.h" #include "esphome/components/web_server/list_entities.h" - -#include "web_server_idf.h" +#endif // USE_WEBSERVER namespace esphome { namespace web_server_idf { @@ -273,6 +275,7 @@ void AsyncResponseStream::printf(const char *fmt, ...) { this->print(str); } +#ifdef USE_WEBSERVER AsyncEventSource::~AsyncEventSource() { for (auto *ses : this->sessions_) { delete ses; // NOLINT(cppcoreguidelines-owning-memory) @@ -511,6 +514,7 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e } } } +#endif } // namespace web_server_idf } // namespace esphome diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 13a3ef168d..d883c0ca9b 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -1,6 +1,7 @@ #pragma once #ifdef USE_ESP_IDF +#include "esphome/core/defines.h" #include #include @@ -12,10 +13,12 @@ #include namespace esphome { +#ifdef USE_WEBSERVER namespace web_server { class WebServer; class ListEntitiesIterator; }; // namespace web_server +#endif namespace web_server_idf { #define F(string_literal) (string_literal) @@ -220,6 +223,7 @@ class AsyncWebHandler { virtual bool isRequestHandlerTrivial() { return true; } }; +#ifdef USE_WEBSERVER class AsyncEventSource; class AsyncEventSourceResponse; @@ -307,10 +311,13 @@ class AsyncEventSource : public AsyncWebHandler { connect_handler_t on_connect_{}; esphome::web_server::WebServer *web_server_; }; +#endif // USE_WEBSERVER class DefaultHeaders { friend class AsyncWebServerRequest; +#ifdef USE_WEBSERVER friend class AsyncEventSourceResponse; +#endif public: // NOLINTNEXTLINE(readability-identifier-naming) From ee37d2f9c8e1ec09f5c77a9a3308717314c59693 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Sat, 14 Jun 2025 15:21:39 +0200 Subject: [PATCH 015/293] Build with C++17 (#8603) Co-authored-by: J. Nick Koston --- esphome/codegen.py | 2 + esphome/components/esp32/__init__.py | 1 + esphome/components/esp8266/__init__.py | 1 + esphome/components/libretiny/__init__.py | 1 + esphome/components/rp2040/__init__.py | 1 + esphome/core/__init__.py | 9 ++- esphome/cpp_generator.py | 11 ++++ esphome/writer.py | 3 + platformio.ini | 75 ++++++++++++++++++++++++ 9 files changed, 103 insertions(+), 1 deletion(-) diff --git a/esphome/codegen.py b/esphome/codegen.py index bfa1683ce7..8e02ec1164 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -22,6 +22,7 @@ from esphome.cpp_generator import ( # noqa: F401 TemplateArguments, add, add_build_flag, + add_build_unflag, add_define, add_global, add_library, @@ -34,6 +35,7 @@ from esphome.cpp_generator import ( # noqa: F401 process_lambda, progmem_array, safe_exp, + set_cpp_standard, statement, static_const_array, templatable, diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 157fd9db11..7f2d718d35 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -695,6 +695,7 @@ FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate) async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) + cg.set_cpp_standard("gnu++17") cg.add_build_flag("-DUSE_ESP32") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index c949e53aa6..4b4862a1d0 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -183,6 +183,7 @@ async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_build_flag("-DUSE_ESP8266") + cg.set_cpp_standard("gnu++17") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "ESP8266") diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 5bdfb15e19..7683c29c63 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -264,6 +264,7 @@ async def component_to_code(config): # force using arduino framework cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") + cg.set_cpp_standard("gnu++17") # disable library compatibility checks cg.add_platformio_option("lib_ldf_mode", "off") diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index c3e11336a9..2718e3050f 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -167,6 +167,7 @@ async def to_code(config): cg.add_platformio_option("lib_ldf_mode", "chain+") cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_build_flag("-DUSE_RP2040") + cg.set_cpp_standard("gnu++17") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "RP2040") diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index e95bd7edcc..bc98ff54db 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -507,6 +507,8 @@ class EsphomeCore: self.libraries: list[Library] = [] # A set of build flags to set in the platformio project self.build_flags: set[str] = set() + # A set of build unflags to set in the platformio project + self.build_unflags: set[str] = set() # A set of defines to set for the compile process in esphome/core/defines.h self.defines: set[Define] = set() # A map of all platformio options to apply @@ -545,6 +547,7 @@ class EsphomeCore: self.global_statements = [] self.libraries = [] self.build_flags = set() + self.build_unflags = set() self.defines = set() self.platformio_options = {} self.loaded_integrations = set() @@ -766,11 +769,15 @@ class EsphomeCore: self.libraries.append(library) return library - def add_build_flag(self, build_flag): + def add_build_flag(self, build_flag: str) -> str: self.build_flags.add(build_flag) _LOGGER.debug("Adding build flag: %s", build_flag) return build_flag + def add_build_unflag(self, build_unflag: str) -> None: + self.build_unflags.add(build_unflag) + _LOGGER.debug("Adding build unflag: %s", build_unflag) + def add_define(self, define): if isinstance(define, str): define = Define(define) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index bbfa6af815..4641f69bdd 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -608,6 +608,17 @@ def add_build_flag(build_flag: str): CORE.add_build_flag(build_flag) +def add_build_unflag(build_unflag: str) -> None: + """Add a global build unflag to the compiler flags.""" + CORE.add_build_unflag(build_unflag) + + +def set_cpp_standard(standard: str) -> None: + """Set C++ standard with compiler flag `-std={standard}`.""" + CORE.add_build_unflag("-std=gnu++11") + CORE.add_build_flag(f"-std={standard}") + + def add_define(name: str, value: SafeExpType = None): """Add a global define to the auto-generated defines.h file. diff --git a/esphome/writer.py b/esphome/writer.py index a47112e1fd..7a5089e384 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -153,6 +153,9 @@ def get_ini_content(): # Sort to avoid changing build flags order CORE.add_platformio_option("build_flags", sorted(CORE.build_flags)) + # Sort to avoid changing build unflags order + CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags)) + content = "[platformio]\n" content += f"description = ESPHome {__version__}\n" diff --git a/platformio.ini b/platformio.ini index 27da883ab3..77938424f9 100644 --- a/platformio.ini +++ b/platformio.ini @@ -48,6 +48,9 @@ lib_deps = lvgl/lvgl@8.4.0 ; lvgl build_flags = -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE + -std=gnu++17 +build_unflags = + -std=gnu++11 src_filter = +<./> +<../tests/dummy_main.cpp> @@ -73,6 +76,8 @@ lib_deps = build_flags = ${common.build_flags} -DUSE_ARDUINO +build_unflags = + ${common.build_unflags} ; This are common settings for all IDF-framework based environments. [common:idf] @@ -80,6 +85,8 @@ extends = common build_flags = ${common.build_flags} -DUSE_ESP_IDF +build_unflags = + ${common.build_unflags} ; This are common settings for the ESP8266 using Arduino. [common:esp8266-arduino] @@ -104,6 +111,8 @@ build_flags = -Wno-nonnull-compare -DUSE_ESP8266 -DUSE_ESP8266_FRAMEWORK_ARDUINO +build_unflags = + ${common.build_unflags} extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. @@ -135,6 +144,8 @@ build_flags = -DUSE_ESP32 -DUSE_ESP32_FRAMEWORK_ARDUINO -DAUDIO_NO_SD_FS ; i2s_audio +build_unflags = + ${common.build_unflags} extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 (all variants) using IDF. @@ -155,6 +166,8 @@ build_flags = -Wno-nonnull-compare -DUSE_ESP32 -DUSE_ESP32_FRAMEWORK_ESP_IDF +build_unflags = + ${common.build_unflags} extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 using the latest ESP-IDF version. @@ -181,6 +194,8 @@ build_flags = ${common:arduino.build_flags} -DUSE_RP2040 -DUSE_RP2040_FRAMEWORK_ARDUINO +build_unflags = + ${common.build_unflags} ; This are common settings for the LibreTiny (all variants) using Arduino. [common:libretiny-arduino] @@ -192,6 +207,8 @@ lib_deps = build_flags = ${common:arduino.build_flags} -DUSE_LIBRETINY +build_unflags = + ${common.build_unflags} build_src_flags = -include Arduino.h ; This is the common settings for the nRF52 using Zephyr. @@ -224,6 +241,8 @@ board = nodemcuv2 build_flags = ${common:esp8266-arduino.build_flags} ${flags:runtime.build_flags} +build_unflags = + ${common.build_unflags} [env:esp8266-arduino-tidy] extends = common:esp8266-arduino @@ -231,6 +250,8 @@ board = nodemcuv2 build_flags = ${common:esp8266-arduino.build_flags} ${flags:clangtidy.build_flags} +build_unflags = + ${common.build_unflags} ;;;;;;;; ESP32 ;;;;;;;; @@ -242,6 +263,8 @@ build_flags = ${common:esp32-arduino.build_flags} ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32 +build_unflags = + ${common.build_unflags} [env:esp32-arduino-tidy] extends = common:esp32-arduino @@ -250,6 +273,8 @@ build_flags = ${common:esp32-arduino.build_flags} ${flags:clangtidy.build_flags} -DUSE_ESP32_VARIANT_ESP32 +build_unflags = + ${common.build_unflags} [env:esp32-idf] extends = common:esp32-idf @@ -259,6 +284,8 @@ build_flags = ${common:esp32-idf.build_flags} ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32 +build_unflags = + ${common.build_unflags} [env:esp32-idf-5_3] extends = common:esp32-idf-5_3 @@ -268,6 +295,8 @@ build_flags = ${common:esp32-idf.build_flags} ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32 +build_unflags = + ${common.build_unflags} [env:esp32-idf-tidy] extends = common:esp32-idf @@ -277,6 +306,8 @@ build_flags = ${common:esp32-idf.build_flags} ${flags:clangtidy.build_flags} -DUSE_ESP32_VARIANT_ESP32 +build_unflags = + ${common.build_unflags} ;;;;;;;; ESP32-C3 ;;;;;;;; @@ -287,6 +318,8 @@ build_flags = ${common:esp32-arduino.build_flags} ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32C3 +build_unflags = + ${common.build_unflags} [env:esp32c3-arduino-tidy] extends = common:esp32-arduino @@ -295,6 +328,8 @@ build_flags = ${common:esp32-arduino.build_flags} ${flags:clangtidy.build_flags} -DUSE_ESP32_VARIANT_ESP32C3 +build_unflags = + ${common.build_unflags} [env:esp32c3-idf] extends = common:esp32-idf @@ -304,6 +339,8 @@ build_flags = ${common:esp32-idf.build_flags} ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32C3 +build_unflags = + ${common.build_unflags} [env:esp32c3-idf-5_3] extends = common:esp32-idf-5_3 @@ -313,6 +350,8 @@ build_flags = ${common:esp32-idf.build_flags} ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32C3 +build_unflags = + ${common.build_unflags} [env:esp32c3-idf-tidy] extends = common:esp32-idf @@ -322,6 +361,8 @@ build_flags = ${common:esp32-idf.build_flags} ${flags:clangtidy.build_flags} -DUSE_ESP32_VARIANT_ESP32C3 +build_unflags = + ${common.build_unflags} ;;;;;;;; ESP32-C6 ;;;;;;;; @@ -343,6 +384,8 @@ build_flags = ${common:esp32-arduino.build_flags} ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32S2 +build_unflags = + ${common.build_unflags} [env:esp32s2-arduino-tidy] extends = common:esp32-arduino @@ -351,6 +394,8 @@ build_flags = ${common:esp32-arduino.build_flags} ${flags:clangtidy.build_flags} -DUSE_ESP32_VARIANT_ESP32S2 +build_unflags = + ${common.build_unflags} [env:esp32s2-idf] extends = common:esp32-idf @@ -360,6 +405,8 @@ build_flags = ${common:esp32-idf.build_flags} ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32S2 +build_unflags = + ${common.build_unflags} [env:esp32s2-idf-5_3] extends = common:esp32-idf-5_3 @@ -369,6 +416,8 @@ build_flags = ${common:esp32-idf.build_flags} ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32S2 +build_unflags = + ${common.build_unflags} [env:esp32s2-idf-tidy] extends = common:esp32-idf @@ -378,6 +427,8 @@ build_flags = ${common:esp32-idf.build_flags} ${flags:clangtidy.build_flags} -DUSE_ESP32_VARIANT_ESP32S2 +build_unflags = + ${common.build_unflags} ;;;;;;;; ESP32-S3 ;;;;;;;; @@ -388,6 +439,8 @@ build_flags = ${common:esp32-arduino.build_flags} ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32S3 +build_unflags = + ${common.build_unflags} [env:esp32s3-arduino-tidy] extends = common:esp32-arduino @@ -396,6 +449,8 @@ build_flags = ${common:esp32-arduino.build_flags} ${flags:clangtidy.build_flags} -DUSE_ESP32_VARIANT_ESP32S3 +build_unflags = + ${common.build_unflags} [env:esp32s3-idf] extends = common:esp32-idf @@ -405,6 +460,8 @@ build_flags = ${common:esp32-idf.build_flags} ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32S3 +build_unflags = + ${common.build_unflags} [env:esp32s3-idf-5_3] extends = common:esp32-idf-5_3 @@ -414,6 +471,8 @@ build_flags = ${common:esp32-idf.build_flags} ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32S3 +build_unflags = + ${common.build_unflags} [env:esp32s3-idf-tidy] extends = common:esp32-idf @@ -423,6 +482,8 @@ build_flags = ${common:esp32-idf.build_flags} ${flags:clangtidy.build_flags} -DUSE_ESP32_VARIANT_ESP32S3 +build_unflags = + ${common.build_unflags} ;;;;;;;; ESP32-P4 ;;;;;;;; @@ -444,6 +505,8 @@ board = rpipico build_flags = ${common:rp2040-arduino.build_flags} ${flags:runtime.build_flags} +build_unflags = + ${common.build_unflags} ;;;;;;;; LibreTiny ;;;;;;;; @@ -455,6 +518,8 @@ build_flags = ${flags:runtime.build_flags} -DUSE_BK72XX -DUSE_LIBRETINY_VARIANT_BK7231N +build_unflags = + ${common.build_unflags} [env:rtl87xxb-arduino] extends = common:libretiny-arduino @@ -464,6 +529,8 @@ build_flags = ${flags:runtime.build_flags} -DUSE_RTL87XX -DUSE_LIBRETINY_VARIANT_RTL8710B +build_unflags = + ${common.build_unflags} [env:rtl87xxc-arduino] extends = common:libretiny-arduino @@ -473,6 +540,8 @@ build_flags = ${flags:runtime.build_flags} -DUSE_RTL87XX -DUSE_LIBRETINY_VARIANT_RTL8720C +build_unflags = + ${common.build_unflags} [env:host] extends = common @@ -483,6 +552,8 @@ build_flags = ${common.build_flags} -DUSE_HOST -std=c++17 +build_unflags = + ${common.build_unflags} ;;;;;;;; nRF52 ;;;;;;;; @@ -492,6 +563,8 @@ board = adafruit_feather_nrf52840 build_flags = ${common:nrf52-zephyr.build_flags} ${flags:runtime.build_flags} +build_unflags = + ${common.build_unflags} [env:nrf52-tidy] extends = common:nrf52-zephyr @@ -499,3 +572,5 @@ board = adafruit_feather_nrf52840 build_flags = ${common:nrf52-zephyr.build_flags} ${flags:clangtidy.build_flags} +build_unflags = + ${common.build_unflags} From 049c7e00cafd260fe9f2f38e11f688a00aa3b8c0 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 14 Jun 2025 18:23:52 -0500 Subject: [PATCH 016/293] Move some consts to ``const.py`` (#9084) --- esphome/components/ld2410/number/__init__.py | 4 ++-- esphome/components/ld2410/sensor.py | 2 +- esphome/components/ld2410/switch/__init__.py | 2 +- esphome/components/ld2420/number/__init__.py | 4 ++-- esphome/components/ld2420/sensor/__init__.py | 8 ++++++-- esphome/components/ld2450/switch/__init__.py | 2 +- esphome/const.py | 4 ++++ 7 files changed, 17 insertions(+), 9 deletions(-) diff --git a/esphome/components/ld2410/number/__init__.py b/esphome/components/ld2410/number/__init__.py index 1f9c50db1f..ffa4e7e146 100644 --- a/esphome/components/ld2410/number/__init__.py +++ b/esphome/components/ld2410/number/__init__.py @@ -3,6 +3,8 @@ from esphome.components import number import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_MOVE_THRESHOLD, + CONF_STILL_THRESHOLD, CONF_TIMEOUT, DEVICE_CLASS_DISTANCE, DEVICE_CLASS_ILLUMINANCE, @@ -24,8 +26,6 @@ MaxDistanceTimeoutNumber = ld2410_ns.class_("MaxDistanceTimeoutNumber", number.N CONF_MAX_MOVE_DISTANCE_GATE = "max_move_distance_gate" CONF_MAX_STILL_DISTANCE_GATE = "max_still_distance_gate" CONF_LIGHT_THRESHOLD = "light_threshold" -CONF_STILL_THRESHOLD = "still_threshold" -CONF_MOVE_THRESHOLD = "move_threshold" TIMEOUT_GROUP = "timeout" diff --git a/esphome/components/ld2410/sensor.py b/esphome/components/ld2410/sensor.py index 38de1799cc..92245ea9a6 100644 --- a/esphome/components/ld2410/sensor.py +++ b/esphome/components/ld2410/sensor.py @@ -3,6 +3,7 @@ from esphome.components import sensor import esphome.config_validation as cv from esphome.const import ( CONF_LIGHT, + CONF_MOVING_DISTANCE, DEVICE_CLASS_DISTANCE, DEVICE_CLASS_ILLUMINANCE, ENTITY_CATEGORY_DIAGNOSTIC, @@ -17,7 +18,6 @@ from esphome.const import ( from . import CONF_LD2410_ID, LD2410Component DEPENDENCIES = ["ld2410"] -CONF_MOVING_DISTANCE = "moving_distance" CONF_STILL_DISTANCE = "still_distance" CONF_MOVING_ENERGY = "moving_energy" CONF_STILL_ENERGY = "still_energy" diff --git a/esphome/components/ld2410/switch/__init__.py b/esphome/components/ld2410/switch/__init__.py index aecad606be..71b8a40a29 100644 --- a/esphome/components/ld2410/switch/__init__.py +++ b/esphome/components/ld2410/switch/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv from esphome.const import ( + CONF_BLUETOOTH, DEVICE_CLASS_SWITCH, ENTITY_CATEGORY_CONFIG, ICON_BLUETOOTH, @@ -14,7 +15,6 @@ BluetoothSwitch = ld2410_ns.class_("BluetoothSwitch", switch.Switch) EngineeringModeSwitch = ld2410_ns.class_("EngineeringModeSwitch", switch.Switch) CONF_ENGINEERING_MODE = "engineering_mode" -CONF_BLUETOOTH = "bluetooth" CONFIG_SCHEMA = { cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), diff --git a/esphome/components/ld2420/number/__init__.py b/esphome/components/ld2420/number/__init__.py index 1558243cc2..a2637b7b06 100644 --- a/esphome/components/ld2420/number/__init__.py +++ b/esphome/components/ld2420/number/__init__.py @@ -3,6 +3,8 @@ from esphome.components import number import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_MOVE_THRESHOLD, + CONF_STILL_THRESHOLD, DEVICE_CLASS_DISTANCE, ENTITY_CATEGORY_CONFIG, ICON_MOTION_SENSOR, @@ -31,8 +33,6 @@ LD2420StillThresholdNumbers = ld2420_ns.class_( ) CONF_MIN_GATE_DISTANCE = "min_gate_distance" CONF_MAX_GATE_DISTANCE = "max_gate_distance" -CONF_STILL_THRESHOLD = "still_threshold" -CONF_MOVE_THRESHOLD = "move_threshold" CONF_GATE_MOVE_SENSITIVITY = "gate_move_sensitivity" CONF_GATE_STILL_SENSITIVITY = "gate_still_sensitivity" CONF_GATE_SELECT = "gate_select" diff --git a/esphome/components/ld2420/sensor/__init__.py b/esphome/components/ld2420/sensor/__init__.py index e39ca99ae1..6dde35753a 100644 --- a/esphome/components/ld2420/sensor/__init__.py +++ b/esphome/components/ld2420/sensor/__init__.py @@ -1,13 +1,17 @@ import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv -from esphome.const import CONF_ID, DEVICE_CLASS_DISTANCE, UNIT_CENTIMETER +from esphome.const import ( + CONF_ID, + CONF_MOVING_DISTANCE, + DEVICE_CLASS_DISTANCE, + UNIT_CENTIMETER, +) from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns LD2420Sensor = ld2420_ns.class_("LD2420Sensor", sensor.Sensor, cg.Component) -CONF_MOVING_DISTANCE = "moving_distance" CONF_GATE_ENERGY = "gate_energy" CONFIG_SCHEMA = cv.All( diff --git a/esphome/components/ld2450/switch/__init__.py b/esphome/components/ld2450/switch/__init__.py index fb3969cf50..2d76b75781 100644 --- a/esphome/components/ld2450/switch/__init__.py +++ b/esphome/components/ld2450/switch/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv from esphome.const import ( + CONF_BLUETOOTH, DEVICE_CLASS_SWITCH, ENTITY_CATEGORY_CONFIG, ICON_BLUETOOTH, @@ -13,7 +14,6 @@ from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns BluetoothSwitch = ld2450_ns.class_("BluetoothSwitch", switch.Switch) MultiTargetSwitch = ld2450_ns.class_("MultiTargetSwitch", switch.Switch) -CONF_BLUETOOTH = "bluetooth" CONF_MULTI_TARGET = "multi_target" CONFIG_SCHEMA = { diff --git a/esphome/const.py b/esphome/const.py index c01f30c3ff..69d75c81ce 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -89,6 +89,7 @@ CONF_BIT_DEPTH = "bit_depth" CONF_BITS_PER_SAMPLE = "bits_per_sample" CONF_BLOCK = "block" CONF_BLUE = "blue" +CONF_BLUETOOTH = "bluetooth" CONF_BOARD = "board" CONF_BOARD_FLASH_MODE = "board_flash_mode" CONF_BORDER = "border" @@ -527,7 +528,9 @@ CONF_MONTH = "month" CONF_MONTHS = "months" CONF_MOSI_PIN = "mosi_pin" CONF_MOTION = "motion" +CONF_MOVE_THRESHOLD = "move_threshold" CONF_MOVEMENT_COUNTER = "movement_counter" +CONF_MOVING_DISTANCE = "moving_distance" CONF_MQTT = "mqtt" CONF_MQTT_ID = "mqtt_id" CONF_MULTIPLE = "multiple" @@ -835,6 +838,7 @@ CONF_STEP = "step" CONF_STEP_DELAY = "step_delay" CONF_STEP_MODE = "step_mode" CONF_STEP_PIN = "step_pin" +CONF_STILL_THRESHOLD = "still_threshold" CONF_STOP = "stop" CONF_STOP_ACTION = "stop_action" CONF_STORE_BASELINE = "store_baseline" From dcfe7af9d3a1d11606f6538e1181d9f6b48fbd14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Jun 2025 21:44:45 -0500 Subject: [PATCH 017/293] Make ParseOnOffState enum uint8_t (#9083) --- esphome/core/helpers.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 7d25e7d261..477f260bf0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -438,7 +438,7 @@ template::value, int> = 0> std::stri } /// Return values for parse_on_off(). -enum ParseOnOffState { +enum ParseOnOffState : uint8_t { PARSE_NONE = 0, PARSE_ON, PARSE_OFF, From 374c33e8dc1ac29a2960e45d144e7124df51b4f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Jun 2025 21:48:53 -0500 Subject: [PATCH 018/293] Optimize Component and Application state storage from uint32_t to uint8_t (#9082) --- .../components/bme280_base/bme280_base.cpp | 5 +-- esphome/components/kmeteriso/kmeteriso.cpp | 5 +-- .../status_led/light/status_led_light.cpp | 4 +- .../status_led/light/status_led_light.h | 2 +- esphome/components/weikai/weikai.cpp | 2 +- esphome/core/application.cpp | 4 +- esphome/core/application.h | 4 +- esphome/core/component.cpp | 36 +++++++++++------ esphome/core/component.h | 39 +++++++++++++------ 9 files changed, 65 insertions(+), 36 deletions(-) diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index 142a03fe1c..d2524e5aac 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -93,9 +93,8 @@ void BME280Component::setup() { // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries // and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component. - if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; + if (this->is_failed()) { + this->reset_to_construction_state(); } if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { diff --git a/esphome/components/kmeteriso/kmeteriso.cpp b/esphome/components/kmeteriso/kmeteriso.cpp index b3fbc31fe6..714df0b538 100644 --- a/esphome/components/kmeteriso/kmeteriso.cpp +++ b/esphome/components/kmeteriso/kmeteriso.cpp @@ -19,9 +19,8 @@ void KMeterISOComponent::setup() { // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries // and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component. - if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; + if (this->is_failed()) { + this->reset_to_construction_state(); } auto err = this->bus_->writev(this->address_, nullptr, 0); diff --git a/esphome/components/status_led/light/status_led_light.cpp b/esphome/components/status_led/light/status_led_light.cpp index 6d38833ebd..dc4820f6da 100644 --- a/esphome/components/status_led/light/status_led_light.cpp +++ b/esphome/components/status_led/light/status_led_light.cpp @@ -9,10 +9,10 @@ namespace status_led { static const char *const TAG = "status_led"; void StatusLEDLightOutput::loop() { - uint32_t new_state = App.get_app_state() & STATUS_LED_MASK; + uint8_t new_state = App.get_app_state() & STATUS_LED_MASK; if (new_state != this->last_app_state_) { - ESP_LOGV(TAG, "New app state 0x%08" PRIX32, new_state); + ESP_LOGV(TAG, "New app state 0x%02X", new_state); } if ((new_state & STATUS_LED_ERROR) != 0u) { diff --git a/esphome/components/status_led/light/status_led_light.h b/esphome/components/status_led/light/status_led_light.h index e711a2e749..bfa144526a 100644 --- a/esphome/components/status_led/light/status_led_light.h +++ b/esphome/components/status_led/light/status_led_light.h @@ -36,7 +36,7 @@ class StatusLEDLightOutput : public light::LightOutput, public Component { GPIOPin *pin_{nullptr}; output::BinaryOutput *output_{nullptr}; light::LightState *lightstate_{}; - uint32_t last_app_state_{0xFFFF}; + uint8_t last_app_state_{0xFF}; void output_state_(bool state); }; diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index 2211fc77d5..ebe987cc65 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -102,7 +102,7 @@ WeikaiRegister &WeikaiRegister::operator|=(uint8_t value) { // The WeikaiComponent methods /////////////////////////////////////////////////////////////////////////////// void WeikaiComponent::loop() { - if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP) + if (!this->is_in_loop_state()) return; // If there are some bytes in the receive FIFO we transfers them to the ring buffers diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 87e6f33e04..4ed96f7300 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -66,7 +66,7 @@ void Application::setup() { [](Component *a, Component *b) { return a->get_loop_priority() > b->get_loop_priority(); }); do { - uint32_t new_app_state = STATUS_LED_WARNING; + uint8_t new_app_state = STATUS_LED_WARNING; this->scheduler.call(); this->feed_wdt(); for (uint32_t j = 0; j <= i; j++) { @@ -87,7 +87,7 @@ void Application::setup() { this->calculate_looping_components_(); } void Application::loop() { - uint32_t new_app_state = 0; + uint8_t new_app_state = 0; this->scheduler.call(); diff --git a/esphome/core/application.h b/esphome/core/application.h index 6c09b25590..d9ef4fe036 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -332,7 +332,7 @@ class Application { */ void teardown_components(uint32_t timeout_ms); - uint32_t get_app_state() const { return this->app_state_; } + uint8_t get_app_state() const { return this->app_state_; } #ifdef USE_BINARY_SENSOR const std::vector &get_binary_sensors() { return this->binary_sensors_; } @@ -653,7 +653,7 @@ class Application { uint32_t last_loop_{0}; uint32_t loop_interval_{16}; size_t dump_config_at_{SIZE_MAX}; - uint32_t app_state_{0}; + uint8_t app_state_{0}; Component *current_component_{nullptr}; uint32_t loop_component_start_time_{0}; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 1141e4067d..dae99a0d22 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -29,15 +29,17 @@ const float LATE = -100.0f; } // namespace setup_priority -const uint32_t COMPONENT_STATE_MASK = 0xFF; -const uint32_t COMPONENT_STATE_CONSTRUCTION = 0x00; -const uint32_t COMPONENT_STATE_SETUP = 0x01; -const uint32_t COMPONENT_STATE_LOOP = 0x02; -const uint32_t COMPONENT_STATE_FAILED = 0x03; -const uint32_t STATUS_LED_MASK = 0xFF00; -const uint32_t STATUS_LED_OK = 0x0000; -const uint32_t STATUS_LED_WARNING = 0x0100; -const uint32_t STATUS_LED_ERROR = 0x0200; +// Component state uses bits 0-1 (4 states) +const uint8_t COMPONENT_STATE_MASK = 0x03; +const uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00; +const uint8_t COMPONENT_STATE_SETUP = 0x01; +const uint8_t COMPONENT_STATE_LOOP = 0x02; +const uint8_t COMPONENT_STATE_FAILED = 0x03; +// Status LED uses bits 2-3 +const uint8_t STATUS_LED_MASK = 0x0C; +const uint8_t STATUS_LED_OK = 0x00; +const uint8_t STATUS_LED_WARNING = 0x04; // Bit 2 +const uint8_t STATUS_LED_ERROR = 0x08; // Bit 3 const uint32_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning const uint32_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again @@ -86,9 +88,9 @@ void Component::call_dump_config() { } } -uint32_t Component::get_component_state() const { return this->component_state_; } +uint8_t Component::get_component_state() const { return this->component_state_; } void Component::call() { - uint32_t state = this->component_state_ & COMPONENT_STATE_MASK; + uint8_t state = this->component_state_ & COMPONENT_STATE_MASK; switch (state) { case COMPONENT_STATE_CONSTRUCTION: // State Construction: Call setup and set state to setup @@ -131,6 +133,18 @@ void Component::mark_failed() { this->component_state_ |= COMPONENT_STATE_FAILED; this->status_set_error(); } +void Component::reset_to_construction_state() { + if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { + ESP_LOGI(TAG, "Component %s is being reset to construction state.", this->get_component_source()); + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; + // Clear error status when resetting + this->status_clear_error(); + } +} +bool Component::is_in_loop_state() const { + return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP; +} void Component::defer(std::function &&f) { // NOLINT App.scheduler.set_timeout(this, "", 0, std::move(f)); } diff --git a/esphome/core/component.h b/esphome/core/component.h index ce9f0289d0..7ad4a5e496 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -53,15 +53,15 @@ static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; ESP_LOGCONFIG(TAG, " Update Interval: %.1fs", this->get_update_interval() / 1000.0f); \ } -extern const uint32_t COMPONENT_STATE_MASK; -extern const uint32_t COMPONENT_STATE_CONSTRUCTION; -extern const uint32_t COMPONENT_STATE_SETUP; -extern const uint32_t COMPONENT_STATE_LOOP; -extern const uint32_t COMPONENT_STATE_FAILED; -extern const uint32_t STATUS_LED_MASK; -extern const uint32_t STATUS_LED_OK; -extern const uint32_t STATUS_LED_WARNING; -extern const uint32_t STATUS_LED_ERROR; +extern const uint8_t COMPONENT_STATE_MASK; +extern const uint8_t COMPONENT_STATE_CONSTRUCTION; +extern const uint8_t COMPONENT_STATE_SETUP; +extern const uint8_t COMPONENT_STATE_LOOP; +extern const uint8_t COMPONENT_STATE_FAILED; +extern const uint8_t STATUS_LED_MASK; +extern const uint8_t STATUS_LED_OK; +extern const uint8_t STATUS_LED_WARNING; +extern const uint8_t STATUS_LED_ERROR; enum class RetryResult { DONE, RETRY }; @@ -123,7 +123,19 @@ class Component { */ virtual void on_powerdown() {} - uint32_t get_component_state() const; + uint8_t get_component_state() const; + + /** Reset this component back to the construction state to allow setup to run again. + * + * This can be used by components that have recoverable failures to attempt setup again. + */ + void reset_to_construction_state(); + + /** Check if this component has completed setup and is in the loop state. + * + * @return True if in loop state, false otherwise. + */ + bool is_in_loop_state() const; /** Mark this component as failed. Any future timeouts/intervals/setup/loop will no longer be called. * @@ -298,7 +310,12 @@ class Component { /// Cancel a defer callback using the specified name, name must not be empty. bool cancel_defer(const std::string &name); // NOLINT - uint32_t component_state_{0x0000}; ///< State of this component. + /// State of this component - each bit has a purpose: + /// Bits 0-1: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED) + /// Bit 2: STATUS_LED_WARNING + /// Bit 3: STATUS_LED_ERROR + /// Bits 4-7: Unused - reserved for future expansion (50% of the bits are free) + uint8_t component_state_{0x00}; float setup_priority_override_{NAN}; const char *component_source_{nullptr}; uint32_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; From a1e4143600a86ff6fd60633b0e8c43beae7e5d21 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Jun 2025 21:55:03 -0500 Subject: [PATCH 019/293] Small optimizations to api buffer helper (#9071) --- esphome/components/api/api_connection.h | 40 +++++++++++-------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 34c7dcd880..13e6066788 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -240,8 +240,8 @@ class APIConnection : public APIServerConnection { // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) // - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext) shared_buf.reserve(reserve_size + header_padding + this->helper_->frame_footer_size()); - // Insert header padding bytes so message encoding starts at the correct position - shared_buf.insert(shared_buf.begin(), header_padding, 0); + // Resize to add header padding so message encoding starts at the correct position + shared_buf.resize(header_padding); return {&shared_buf}; } @@ -249,32 +249,26 @@ class APIConnection : public APIServerConnection { ProtoWriteBuffer prepare_message_buffer(uint16_t message_size, bool is_first_message) { // Get reference to shared buffer (it maintains state between batch messages) std::vector &shared_buf = this->parent_->get_shared_buffer_ref(); - size_t current_size = shared_buf.size(); if (is_first_message) { - // For first message, initialize buffer with header padding - uint8_t header_padding = this->helper_->frame_header_padding(); shared_buf.clear(); - shared_buf.reserve(message_size + header_padding); - shared_buf.resize(header_padding); - // Fill header padding with zeros - std::fill(shared_buf.begin(), shared_buf.end(), 0); - } else { - // For subsequent messages, add footer space for previous message and header for this message - uint8_t footer_size = this->helper_->frame_footer_size(); - uint8_t header_padding = this->helper_->frame_header_padding(); - - // Reserve additional space for everything - shared_buf.reserve(current_size + footer_size + header_padding + message_size); - - // Single resize to add both footer and header padding - size_t new_size = current_size + footer_size + header_padding; - shared_buf.resize(new_size); - - // Fill the newly added bytes with zeros (footer + header padding) - std::fill(shared_buf.begin() + current_size, shared_buf.end(), 0); } + size_t current_size = shared_buf.size(); + + // Calculate padding to add: + // - First message: just header padding + // - Subsequent messages: footer for previous message + header padding for this message + size_t padding_to_add = is_first_message + ? this->helper_->frame_header_padding() + : this->helper_->frame_header_padding() + this->helper_->frame_footer_size(); + + // Reserve space for padding + message + shared_buf.reserve(current_size + padding_to_add + message_size); + + // Resize to add the padding bytes + shared_buf.resize(current_size + padding_to_add); + return {&shared_buf}; } From 4305c444409c58190391e52d18b52c999a95a7a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Jun 2025 22:21:55 -0500 Subject: [PATCH 020/293] Reduce entity memory usage by eliminating field shadowing and bit-packing (#9076) --- esphome/components/datetime/date_entity.cpp | 10 +- esphome/components/datetime/datetime_base.h | 5 - .../components/datetime/datetime_entity.cpp | 16 +-- esphome/components/datetime/time_entity.cpp | 8 +- .../components/esp32_camera/esp32_camera.cpp | 2 +- .../binary_sensor/nextion_binarysensor.cpp | 2 +- .../nextion/sensor/nextion_sensor.cpp | 2 +- .../text_sensor/nextion_textsensor.cpp | 2 +- esphome/components/number/number.cpp | 2 +- esphome/components/number/number.h | 4 - esphome/components/select/select.cpp | 2 +- esphome/components/select/select.h | 4 - esphome/components/sensor/sensor.cpp | 3 +- esphome/components/sensor/sensor.h | 4 - esphome/components/text/text.cpp | 2 +- esphome/components/text/text.h | 4 - .../components/text_sensor/text_sensor.cpp | 3 +- esphome/components/text_sensor/text_sensor.h | 4 - esphome/components/update/update_entity.cpp | 2 +- esphome/components/update/update_entity.h | 3 - .../uptime/sensor/uptime_timestamp_sensor.cpp | 2 +- esphome/core/entity_base.cpp | 20 +--- esphome/core/entity_base.h | 37 ++++-- .../fixtures/host_mode_entity_fields.yaml | 108 ++++++++++++++++++ .../test_host_mode_entity_fields.py | 93 +++++++++++++++ 25 files changed, 258 insertions(+), 86 deletions(-) create mode 100644 tests/integration/fixtures/host_mode_entity_fields.yaml create mode 100644 tests/integration/test_host_mode_entity_fields.py diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index b5bcef43af..c164a98b2e 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -11,25 +11,25 @@ static const char *const TAG = "datetime.date_entity"; void DateEntity::publish_state() { if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { - this->has_state_ = false; + this->set_has_state(false); return; } if (this->year_ < 1970 || this->year_ > 3000) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Year must be between 1970 and 3000"); return; } if (this->month_ < 1 || this->month_ > 12) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Month must be between 1 and 12"); return; } if (this->day_ > days_in_month(this->month_, this->year_)) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_); return; } - this->has_state_ = true; + this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); this->state_callback_.call(); } diff --git a/esphome/components/datetime/datetime_base.h b/esphome/components/datetime/datetime_base.h index dea34e6110..b7645f5539 100644 --- a/esphome/components/datetime/datetime_base.h +++ b/esphome/components/datetime/datetime_base.h @@ -13,9 +13,6 @@ namespace datetime { class DateTimeBase : public EntityBase { public: - /// Return whether this Datetime has gotten a full state yet. - bool has_state() const { return this->has_state_; } - virtual ESPTime state_as_esptime() const = 0; void add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } @@ -31,8 +28,6 @@ class DateTimeBase : public EntityBase { #ifdef USE_TIME time::RealTimeClock *rtc_; #endif - - bool has_state_{false}; }; #ifdef USE_TIME diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index 3d92194efa..4e3b051eb3 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -11,40 +11,40 @@ static const char *const TAG = "datetime.datetime_entity"; void DateTimeEntity::publish_state() { if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { - this->has_state_ = false; + this->set_has_state(false); return; } if (this->year_ < 1970 || this->year_ > 3000) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Year must be between 1970 and 3000"); return; } if (this->month_ < 1 || this->month_ > 12) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Month must be between 1 and 12"); return; } if (this->day_ > days_in_month(this->month_, this->year_)) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_); return; } if (this->hour_ > 23) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Hour must be between 0 and 23"); return; } if (this->minute_ > 59) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Minute must be between 0 and 59"); return; } if (this->second_ > 59) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Second must be between 0 and 59"); return; } - this->has_state_ = true; + this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_, this->day_, this->hour_, this->minute_, this->second_); this->state_callback_.call(); diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp index db0094ae01..9b05c2124f 100644 --- a/esphome/components/datetime/time_entity.cpp +++ b/esphome/components/datetime/time_entity.cpp @@ -11,21 +11,21 @@ static const char *const TAG = "datetime.time_entity"; void TimeEntity::publish_state() { if (this->hour_ > 23) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Hour must be between 0 and 23"); return; } if (this->minute_ > 59) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Minute must be between 0 and 59"); return; } if (this->second_ > 59) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Second must be between 0 and 59"); return; } - this->has_state_ = true; + this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_); this->state_callback_.call(); diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index a7551571dd..da0f277358 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -57,7 +57,7 @@ void ESP32Camera::dump_config() { " External Clock: Pin:%d Frequency:%u\n" " I2C Pins: SDA:%d SCL:%d\n" " Reset Pin: %d", - this->name_.c_str(), YESNO(this->internal_), conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3, + this->name_.c_str(), YESNO(this->is_internal()), conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3, conf.pin_d4, conf.pin_d5, conf.pin_d6, conf.pin_d7, conf.pin_vsync, conf.pin_href, conf.pin_pclk, conf.pin_xclk, conf.xclk_freq_hz, conf.pin_sccb_sda, conf.pin_sccb_scl, conf.pin_reset); switch (this->config_.frame_size) { diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp index ab1e20859c..b6d4cc3f23 100644 --- a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp @@ -56,7 +56,7 @@ void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nexti this->publish_state(state); } else { this->state = state; - this->has_state_ = true; + this->set_has_state(true); } this->update_component_settings(); diff --git a/esphome/components/nextion/sensor/nextion_sensor.cpp b/esphome/components/nextion/sensor/nextion_sensor.cpp index 9be49e3476..0ed9da95d4 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.cpp +++ b/esphome/components/nextion/sensor/nextion_sensor.cpp @@ -88,7 +88,7 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { } else { this->raw_state = state; this->state = state; - this->has_state_ = true; + this->set_has_state(true); } } this->update_component_settings(); diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp index a1d45f55e0..e08cbb02ca 100644 --- a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp @@ -37,7 +37,7 @@ void NextionTextSensor::set_state(const std::string &state, bool publish, bool s this->publish_state(state); } else { this->state = state; - this->has_state_ = true; + this->set_has_state(true); } this->update_component_settings(); diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index fda4f43e34..b6a845b19b 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -7,7 +7,7 @@ namespace number { static const char *const TAG = "number"; void Number::publish_state(float state) { - this->has_state_ = true; + this->set_has_state(true); this->state = state; ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state); this->state_callback_.call(state); diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index d839d12ad1..49bcbb857c 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -48,9 +48,6 @@ class Number : public EntityBase { NumberTraits traits; - /// Return whether this number has gotten a full state yet. - bool has_state() const { return has_state_; } - protected: friend class NumberCall; @@ -63,7 +60,6 @@ class Number : public EntityBase { virtual void control(float value) = 0; CallbackManager state_callback_; - bool has_state_{false}; }; } // namespace number diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 806882ad94..37887da27c 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -10,7 +10,7 @@ void Select::publish_state(const std::string &state) { auto index = this->index_of(state); const auto *name = this->get_name().c_str(); if (index.has_value()) { - this->has_state_ = true; + this->set_has_state(true); this->state = state; ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", name, state.c_str(), index.value()); this->state_callback_.call(state, index.value()); diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 8ca9a69d1c..3ab651b241 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -35,9 +35,6 @@ class Select : public EntityBase { void publish_state(const std::string &state); - /// Return whether this select component has gotten a full state yet. - bool has_state() const { return has_state_; } - /// Instantiate a SelectCall object to modify this select component's state. SelectCall make_call() { return SelectCall(this); } @@ -73,7 +70,6 @@ class Select : public EntityBase { virtual void control(const std::string &value) = 0; CallbackManager state_callback_; - bool has_state_{false}; }; } // namespace select diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 14a8b3d490..251ef47ecc 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -88,13 +88,12 @@ float Sensor::get_raw_state() const { return this->raw_state; } std::string Sensor::unique_id() { return ""; } void Sensor::internal_send_state_to_frontend(float state) { - this->has_state_ = true; + this->set_has_state(true); this->state = state; ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state, this->get_unit_of_measurement().c_str(), this->get_accuracy_decimals()); this->callback_.call(state); } -bool Sensor::has_state() const { return this->has_state_; } } // namespace sensor } // namespace esphome diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index ab9ff1565c..ac61548a55 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -140,9 +140,6 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa */ float raw_state; - /// Return whether this sensor has gotten a full state (that passed through all filters) yet. - bool has_state() const; - /** Override this method to set the unique ID of this sensor. * * @deprecated Do not use for new sensors, a suitable unique ID is automatically generated (2023.4). @@ -160,7 +157,6 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa optional accuracy_decimals_; ///< Accuracy in decimals override optional state_class_{STATE_CLASS_NONE}; ///< State class override bool force_update_{false}; ///< Force update mode - bool has_state_{false}; }; } // namespace sensor diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index 8f0242e747..654893d4e4 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -7,7 +7,7 @@ namespace text { static const char *const TAG = "text"; void Text::publish_state(const std::string &state) { - this->has_state_ = true; + this->set_has_state(true); this->state = state; if (this->traits.get_mode() == TEXT_MODE_PASSWORD) { ESP_LOGD(TAG, "'%s': Sending state " LOG_SECRET("'%s'"), this->get_name().c_str(), state.c_str()); diff --git a/esphome/components/text/text.h b/esphome/components/text/text.h index f71dde69ba..3cc0cefc3e 100644 --- a/esphome/components/text/text.h +++ b/esphome/components/text/text.h @@ -28,9 +28,6 @@ class Text : public EntityBase { void publish_state(const std::string &state); - /// Return whether this text input has gotten a full state yet. - bool has_state() const { return has_state_; } - /// Instantiate a TextCall object to modify this text component's state. TextCall make_call() { return TextCall(this); } @@ -48,7 +45,6 @@ class Text : public EntityBase { virtual void control(const std::string &value) = 0; CallbackManager state_callback_; - bool has_state_{false}; }; } // namespace text diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index f10cd50267..1138ada281 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -60,13 +60,12 @@ std::string TextSensor::get_state() const { return this->state; } std::string TextSensor::get_raw_state() const { return this->raw_state; } void TextSensor::internal_send_state_to_frontend(const std::string &state) { this->state = state; - this->has_state_ = true; + this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str()); this->callback_.call(state); } std::string TextSensor::unique_id() { return ""; } -bool TextSensor::has_state() { return this->has_state_; } } // namespace text_sensor } // namespace esphome diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index bd72ea70e3..5e45968ef4 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -67,8 +67,6 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { */ virtual std::string unique_id(); - bool has_state(); - void internal_send_state_to_frontend(const std::string &state); protected: @@ -76,8 +74,6 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { CallbackManager callback_; ///< Storage for filtered state callbacks. Filter *filter_list_{nullptr}; ///< Store all active filters. - - bool has_state_{false}; }; } // namespace text_sensor diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp index ed9a0480d8..ce97fb1b77 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -30,7 +30,7 @@ void UpdateEntity::publish_state() { ESP_LOGD(TAG, " Progress: %.0f%%", this->update_info_.progress); } - this->has_state_ = true; + this->set_has_state(true); this->state_callback_.call(); } diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h index cc269e288f..169e580457 100644 --- a/esphome/components/update/update_entity.h +++ b/esphome/components/update/update_entity.h @@ -28,8 +28,6 @@ enum UpdateState : uint8_t { class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { public: - bool has_state() const { return this->has_state_; } - void publish_state(); void perform() { this->perform(false); } @@ -44,7 +42,6 @@ class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { protected: UpdateState state_{UPDATE_STATE_UNKNOWN}; UpdateInfo update_info_; - bool has_state_{false}; CallbackManager state_callback_{}; }; diff --git a/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp b/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp index fa8cb2bb61..69033be11c 100644 --- a/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp +++ b/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp @@ -13,7 +13,7 @@ static const char *const TAG = "uptime.sensor"; void UptimeTimestampSensor::setup() { this->time_->add_on_time_sync_callback([this]() { - if (this->has_state_) + if (this->has_state()) return; // No need to update the timestamp if it's already set auto now = this->time_->now(); diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 725a8569a3..791b6615a1 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -12,20 +12,12 @@ void EntityBase::set_name(const char *name) { this->name_ = StringRef(name); if (this->name_.empty()) { this->name_ = StringRef(App.get_friendly_name()); - this->has_own_name_ = false; + this->flags_.has_own_name = false; } else { - this->has_own_name_ = true; + this->flags_.has_own_name = true; } } -// Entity Internal -bool EntityBase::is_internal() const { return this->internal_; } -void EntityBase::set_internal(bool internal) { this->internal_ = internal; } - -// Entity Disabled by Default -bool EntityBase::is_disabled_by_default() const { return this->disabled_by_default_; } -void EntityBase::set_disabled_by_default(bool disabled_by_default) { this->disabled_by_default_ = disabled_by_default; } - // Entity Icon std::string EntityBase::get_icon() const { if (this->icon_c_str_ == nullptr) { @@ -35,14 +27,10 @@ std::string EntityBase::get_icon() const { } void EntityBase::set_icon(const char *icon) { this->icon_c_str_ = icon; } -// Entity Category -EntityCategory EntityBase::get_entity_category() const { return this->entity_category_; } -void EntityBase::set_entity_category(EntityCategory entity_category) { this->entity_category_ = entity_category; } - // Entity Object ID std::string EntityBase::get_object_id() const { // Check if `App.get_friendly_name()` is constant or dynamic. - if (!this->has_own_name_ && App.is_name_add_mac_suffix_enabled()) { + if (!this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled()) { // `App.get_friendly_name()` is dynamic. return str_sanitize(str_snake_case(App.get_friendly_name())); } else { @@ -61,7 +49,7 @@ void EntityBase::set_object_id(const char *object_id) { // Calculate Object ID Hash from Entity Name void EntityBase::calc_object_id_() { // Check if `App.get_friendly_name()` is constant or dynamic. - if (!this->has_own_name_ && App.is_name_add_mac_suffix_enabled()) { + if (!this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled()) { // `App.get_friendly_name()` is dynamic. const auto object_id = str_sanitize(str_snake_case(App.get_friendly_name())); // FNV-1 hash diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index a2e1d4adbc..0f0d635962 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -22,7 +22,7 @@ class EntityBase { void set_name(const char *name); // Get whether this Entity has its own name or it should use the device friendly_name. - bool has_own_name() const { return this->has_own_name_; } + bool has_own_name() const { return this->flags_.has_own_name; } // Get the sanitized name of this Entity as an ID. std::string get_object_id() const; @@ -32,23 +32,31 @@ class EntityBase { uint32_t get_object_id_hash(); // Get/set whether this Entity should be hidden outside ESPHome - bool is_internal() const; - void set_internal(bool internal); + bool is_internal() const { return this->flags_.internal; } + void set_internal(bool internal) { this->flags_.internal = internal; } // Check if this object is declared to be disabled by default. // That means that when the device gets added to Home Assistant (or other clients) it should // not be added to the default view by default, and a user action is necessary to manually add it. - bool is_disabled_by_default() const; - void set_disabled_by_default(bool disabled_by_default); + bool is_disabled_by_default() const { return this->flags_.disabled_by_default; } + void set_disabled_by_default(bool disabled_by_default) { this->flags_.disabled_by_default = disabled_by_default; } // Get/set the entity category. - EntityCategory get_entity_category() const; - void set_entity_category(EntityCategory entity_category); + EntityCategory get_entity_category() const { return static_cast(this->flags_.entity_category); } + void set_entity_category(EntityCategory entity_category) { + this->flags_.entity_category = static_cast(entity_category); + } // Get/set this entity's icon std::string get_icon() const; void set_icon(const char *icon); + // Check if this entity has state + bool has_state() const { return this->flags_.has_state; } + + // Set has_state - for components that need to manually set this + void set_has_state(bool state) { this->flags_.has_state = state; } + protected: /// The hash_base() function has been deprecated. It is kept in this /// class for now, to prevent external components from not compiling. @@ -59,11 +67,16 @@ class EntityBase { const char *object_id_c_str_{nullptr}; const char *icon_c_str_{nullptr}; uint32_t object_id_hash_{}; - bool has_own_name_{false}; - bool internal_{false}; - bool disabled_by_default_{false}; - EntityCategory entity_category_{ENTITY_CATEGORY_NONE}; - bool has_state_{}; + + // Bit-packed flags to save memory (1 byte instead of 5) + struct EntityFlags { + uint8_t has_own_name : 1; + uint8_t internal : 1; + uint8_t disabled_by_default : 1; + uint8_t has_state : 1; + uint8_t entity_category : 2; // Supports up to 4 categories + uint8_t reserved : 2; // Reserved for future use + } flags_{}; }; class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) diff --git a/tests/integration/fixtures/host_mode_entity_fields.yaml b/tests/integration/fixtures/host_mode_entity_fields.yaml new file mode 100644 index 0000000000..0bd87ee794 --- /dev/null +++ b/tests/integration/fixtures/host_mode_entity_fields.yaml @@ -0,0 +1,108 @@ +esphome: + name: host-test + +host: + +api: + +logger: + +# Test various entity types with different flag combinations +sensor: + - platform: template + name: "Test Normal Sensor" + id: normal_sensor + update_interval: 1s + lambda: |- + return 42.0; + + - platform: template + name: "Test Internal Sensor" + id: internal_sensor + internal: true + update_interval: 1s + lambda: |- + return 43.0; + + - platform: template + name: "Test Disabled Sensor" + id: disabled_sensor + disabled_by_default: true + update_interval: 1s + lambda: |- + return 44.0; + + - platform: template + name: "Test Mixed Flags Sensor" + id: mixed_flags_sensor + internal: true + entity_category: diagnostic + update_interval: 1s + lambda: |- + return 45.0; + + - platform: template + name: "Test Diagnostic Sensor" + id: diagnostic_sensor + entity_category: diagnostic + update_interval: 1s + lambda: |- + return 46.0; + + - platform: template + name: "Test All Flags Sensor" + id: all_flags_sensor + internal: true + disabled_by_default: true + entity_category: diagnostic + update_interval: 1s + lambda: |- + return 47.0; + +# Also test other entity types to ensure bit-packing works across all +binary_sensor: + - platform: template + name: "Test Binary Sensor" + entity_category: config + lambda: |- + return true; + +text_sensor: + - platform: template + name: "Test Text Sensor" + disabled_by_default: true + lambda: |- + return {"Hello"}; + +number: + - platform: template + name: "Test Number" + initial_value: 50 + min_value: 0 + max_value: 100 + step: 1 + optimistic: true + entity_category: diagnostic + +select: + - platform: template + name: "Test Select" + options: + - "Option 1" + - "Option 2" + initial_option: "Option 1" + optimistic: true + internal: true + +switch: + - platform: template + name: "Test Switch" + optimistic: true + disabled_by_default: true + entity_category: config + +button: + - platform: template + name: "Test Button" + on_press: + - logger.log: "Button pressed" diff --git a/tests/integration/test_host_mode_entity_fields.py b/tests/integration/test_host_mode_entity_fields.py new file mode 100644 index 0000000000..cf3fa6916a --- /dev/null +++ b/tests/integration/test_host_mode_entity_fields.py @@ -0,0 +1,93 @@ +"""Integration test for entity bit-packed fields.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityCategory, EntityState +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_host_mode_entity_fields( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test entity bit-packed fields work correctly with all possible values.""" + # Write, compile and run the ESPHome device, then connect to API + async with run_compiled(yaml_config), api_client_connected() as client: + # Get all entities + entities = await client.list_entities_services() + + # Create a map of entity names to entity info + entity_map = {} + for entity in entities[0]: + if hasattr(entity, "name"): + entity_map[entity.name] = entity + + # Test entities that should be visible via API (non-internal) + visible_test_cases = [ + # (entity_name, expected_disabled_by_default, expected_entity_category) + ("Test Normal Sensor", False, EntityCategory.NONE), + ("Test Disabled Sensor", True, EntityCategory.NONE), + ("Test Diagnostic Sensor", False, EntityCategory.DIAGNOSTIC), + ("Test Switch", True, EntityCategory.CONFIG), + ("Test Binary Sensor", False, EntityCategory.CONFIG), + ("Test Number", False, EntityCategory.DIAGNOSTIC), + ] + + # Test entities that should NOT be visible via API (internal) + internal_entities = [ + "Test Internal Sensor", + "Test Mixed Flags Sensor", + "Test All Flags Sensor", + "Test Select", + ] + + # Verify visible entities + for entity_name, expected_disabled, expected_category in visible_test_cases: + assert entity_name in entity_map, ( + f"Entity '{entity_name}' not found - it should be visible via API" + ) + entity = entity_map[entity_name] + + # Check disabled_by_default flag + assert entity.disabled_by_default == expected_disabled, ( + f"{entity_name}: disabled_by_default flag mismatch - " + f"expected {expected_disabled}, got {entity.disabled_by_default}" + ) + + # Check entity_category + assert entity.entity_category == expected_category, ( + f"{entity_name}: entity_category mismatch - " + f"expected {expected_category}, got {entity.entity_category}" + ) + + # Verify internal entities are NOT visible + for entity_name in internal_entities: + assert entity_name not in entity_map, ( + f"Entity '{entity_name}' found in API response - " + f"internal entities should not be exposed via API" + ) + + # Subscribe to states to verify has_state flag works + states: dict[int, EntityState] = {} + state_received = asyncio.Event() + + def on_state(state: EntityState) -> None: + states[state.key] = state + state_received.set() + + client.subscribe_states(on_state) + + # Wait for at least one state + try: + await asyncio.wait_for(state_received.wait(), timeout=5.0) + except asyncio.TimeoutError: + pytest.fail("No states received within 5 seconds") + + # Verify we received states (which means has_state flag is working) + assert len(states) > 0, "No states received - has_state flag may not be working" From cb019fff9a1ff6548bcdd39c9b54e96b968aaa75 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Jun 2025 22:28:15 -0500 Subject: [PATCH 021/293] Optimize memory usage by lazy-allocating raw callbacks in sensors (#9077) --- esphome/components/sensor/sensor.cpp | 9 +++++++-- esphome/components/sensor/sensor.h | 5 +++-- esphome/components/text_sensor/text_sensor.cpp | 9 +++++++-- esphome/components/text_sensor/text_sensor.h | 8 ++++++-- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 251ef47ecc..3be0df9963 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -38,7 +38,9 @@ StateClass Sensor::get_state_class() { void Sensor::publish_state(float state) { this->raw_state = state; - this->raw_callback_.call(state); + if (this->raw_callback_) { + this->raw_callback_->call(state); + } ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state); @@ -51,7 +53,10 @@ void Sensor::publish_state(float state) { void Sensor::add_on_state_callback(std::function &&callback) { this->callback_.add(std::move(callback)); } void Sensor::add_on_raw_state_callback(std::function &&callback) { - this->raw_callback_.add(std::move(callback)); + if (!this->raw_callback_) { + this->raw_callback_ = std::make_unique>(); + } + this->raw_callback_->add(std::move(callback)); } void Sensor::add_filter(Filter *filter) { diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index ac61548a55..456e876497 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -7,6 +7,7 @@ #include "esphome/components/sensor/filter.h" #include +#include namespace esphome { namespace sensor { @@ -149,8 +150,8 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa void internal_send_state_to_frontend(float state); protected: - CallbackManager raw_callback_; ///< Storage for raw state callbacks. - CallbackManager callback_; ///< Storage for filtered state callbacks. + std::unique_ptr> raw_callback_; ///< Storage for raw state callbacks (lazy allocated). + CallbackManager callback_; ///< Storage for filtered state callbacks. Filter *filter_list_{nullptr}; ///< Store all active filters. diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 1138ada281..91cb320782 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -8,7 +8,9 @@ static const char *const TAG = "text_sensor"; void TextSensor::publish_state(const std::string &state) { this->raw_state = state; - this->raw_callback_.call(state); + if (this->raw_callback_) { + this->raw_callback_->call(state); + } ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), state.c_str()); @@ -53,7 +55,10 @@ void TextSensor::add_on_state_callback(std::function callback this->callback_.add(std::move(callback)); } void TextSensor::add_on_raw_state_callback(std::function callback) { - this->raw_callback_.add(std::move(callback)); + if (!this->raw_callback_) { + this->raw_callback_ = std::make_unique>(); + } + this->raw_callback_->add(std::move(callback)); } std::string TextSensor::get_state() const { return this->state; } diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 5e45968ef4..b27145aa18 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -6,6 +6,7 @@ #include "esphome/components/text_sensor/filter.h" #include +#include namespace esphome { namespace text_sensor { @@ -33,6 +34,8 @@ namespace text_sensor { class TextSensor : public EntityBase, public EntityBase_DeviceClass { public: + TextSensor() = default; + /// Getter-syntax for .state. std::string get_state() const; /// Getter-syntax for .raw_state @@ -70,8 +73,9 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { void internal_send_state_to_frontend(const std::string &state); protected: - CallbackManager raw_callback_; ///< Storage for raw state callbacks. - CallbackManager callback_; ///< Storage for filtered state callbacks. + std::unique_ptr> + raw_callback_; ///< Storage for raw state callbacks (lazy allocated). + CallbackManager callback_; ///< Storage for filtered state callbacks. Filter *filter_list_{nullptr}; ///< Store all active filters. }; From 98e268410730ae363551e40fd81fc8b9930525d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Jun 2025 22:46:02 -0500 Subject: [PATCH 022/293] Fix API message encoding to return actual size instead of calculated size (#9073) --- esphome/components/api/api_connection.cpp | 32 +++++++++++++++++------ tests/integration/conftest.py | 15 +++++++++++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index d09b1107d2..ca6e2a2d56 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -248,25 +248,41 @@ void APIConnection::on_disconnect_response(const DisconnectResponse &value) { uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { // Calculate size - uint32_t size = 0; - msg.calculate_size(size); + uint32_t calculated_size = 0; + msg.calculate_size(calculated_size); + + // Cache frame sizes to avoid repeated virtual calls + const uint8_t header_padding = conn->helper_->frame_header_padding(); + const uint8_t footer_size = conn->helper_->frame_footer_size(); // Calculate total size with padding for buffer allocation - uint16_t total_size = - static_cast(size) + conn->helper_->frame_header_padding() + conn->helper_->frame_footer_size(); + size_t total_calculated_size = calculated_size + header_padding + footer_size; // Check if it fits - if (total_size > remaining_size) { + if (total_calculated_size > remaining_size) { return 0; // Doesn't fit } // Allocate buffer space - pass payload size, allocation functions add header/footer space - ProtoWriteBuffer buffer = - is_single ? conn->allocate_single_message_buffer(size) : conn->allocate_batch_message_buffer(size); + ProtoWriteBuffer buffer = is_single ? conn->allocate_single_message_buffer(calculated_size) + : conn->allocate_batch_message_buffer(calculated_size); + + // Get buffer size after allocation (which includes header padding) + std::vector &shared_buf = conn->parent_->get_shared_buffer_ref(); + size_t size_before_encode = shared_buf.size(); // Encode directly into buffer msg.encode(buffer); - return total_size; + + // Calculate actual encoded size (not including header that was already added) + size_t actual_payload_size = shared_buf.size() - size_before_encode; + + // Return actual total size (header + actual payload + footer) + size_t actual_total_size = header_padding + actual_payload_size + footer_size; + + // Verify that calculate_size() returned the correct value + assert(calculated_size == actual_payload_size); + return static_cast(actual_total_size); } #ifdef USE_BINARY_SENSOR diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 4c798c6b72..4eb1584c27 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -119,6 +119,21 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s # Add port configuration after api: content = content.replace("api:", f"api:\n port: {unused_tcp_port}") + # Add debug build flags for integration tests to enable assertions + if "esphome:" in content: + # Check if platformio_options already exists + if "platformio_options:" not in content: + # Add platformio_options with debug flags after esphome: + content = content.replace( + "esphome:", + "esphome:\n" + " # Enable assertions for integration tests\n" + " platformio_options:\n" + " build_flags:\n" + ' - "-DDEBUG" # Enable assert() statements\n' + ' - "-g" # Add debug symbols', + ) + return content From 78e3c6333f97a4f15fef35415eaa1219f2a20652 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Jun 2025 22:46:40 -0500 Subject: [PATCH 023/293] Optimize Application area_ from std::string to const char* (#9085) --- esphome/components/mqtt/mqtt_component.cpp | 2 +- esphome/core/application.h | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 456ae25e65..eee5644c9d 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -153,7 +153,7 @@ bool MQTTComponent::send_discovery_() { if (node_friendly_name.empty()) { node_friendly_name = node_name; } - const std::string &node_area = App.get_area(); + std::string node_area = App.get_area(); JsonObject device_info = root.createNestedObject(MQTT_DEVICE); const auto mac = get_mac_address(); diff --git a/esphome/core/application.h b/esphome/core/application.h index d9ef4fe036..f04ea05d8e 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -87,8 +87,8 @@ static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick class Application { public: - void pre_setup(const std::string &name, const std::string &friendly_name, const std::string &area, - const char *comment, const char *compilation_time, bool name_add_mac_suffix) { + void pre_setup(const std::string &name, const std::string &friendly_name, const char *area, const char *comment, + const char *compilation_time, bool name_add_mac_suffix) { arch_init(); this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { @@ -285,7 +285,7 @@ class Application { const std::string &get_friendly_name() const { return this->friendly_name_; } /// Get the area of this Application set by pre_setup(). - const std::string &get_area() const { return this->area_; } + std::string get_area() const { return this->area_ == nullptr ? "" : this->area_; } /// Get the comment of this Application set by pre_setup(). std::string get_comment() const { return this->comment_; } @@ -646,7 +646,7 @@ class Application { std::string name_; std::string friendly_name_; - std::string area_; + const char *area_{nullptr}; const char *comment_{nullptr}; const char *compilation_time_{nullptr}; bool name_add_mac_suffix_; From 07cf6e723bdd93a738bfc033ad9a932c1bf74b09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Jun 2025 23:45:41 -0500 Subject: [PATCH 024/293] Fix unbound BLE event queue growth and reduce memory usage (#9052) --- .../bluetooth_proxy/bluetooth_proxy.cpp | 4 +- .../bluetooth_proxy/bluetooth_proxy.h | 2 +- esphome/components/esp32_ble/ble.cpp | 119 +++++--- esphome/components/esp32_ble/ble.h | 24 +- esphome/components/esp32_ble/ble_event.h | 276 +++++++++++++----- .../components/esp32_ble/ble_scan_result.h | 24 ++ esphome/components/esp32_ble/queue.h | 11 + .../components/esp32_ble_tracker/__init__.py | 1 + .../esp32_ble_tracker/esp32_ble_tracker.cpp | 91 +++--- .../esp32_ble_tracker/esp32_ble_tracker.h | 20 +- esphome/components/logger/logger.cpp | 17 +- esphome/components/logger/logger.h | 3 +- 12 files changed, 397 insertions(+), 195 deletions(-) create mode 100644 esphome/components/esp32_ble/ble_scan_result.h diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 7aeb818306..fbe2a3e67c 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -58,7 +58,7 @@ static std::vector &get_batch_buffer() { return batch_buffer; } -bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { +bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) { if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_) return false; @@ -73,7 +73,7 @@ bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_p // Add new advertisements to the batch buffer for (size_t i = 0; i < count; i++) { - auto &result = advertisements[i]; + auto &result = scan_results[i]; uint8_t length = result.adv_data_len + result.scan_rsp_len; batch_buffer.emplace_back(); diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index f75e73e796..16db0a0a11 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -52,7 +52,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com public: BluetoothProxy(); bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; - bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override; + bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override; void dump_config() override; void setup() override; void loop() override; diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 824c2b9dbc..ed74d59ef2 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -23,6 +23,9 @@ namespace esp32_ble { static const char *const TAG = "esp32_ble"; +// Maximum size of the BLE event queue +static constexpr size_t MAX_BLE_QUEUE_SIZE = SCAN_RESULT_BUFFER_SIZE * 2; + static RAMAllocator EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) RAMAllocator::ALLOW_FAILURE | RAMAllocator::ALLOC_INTERNAL); @@ -304,20 +307,52 @@ void ESP32BLE::loop() { BLEEvent *ble_event = this->ble_events_.pop(); while (ble_event != nullptr) { switch (ble_event->type_) { - case BLEEvent::GATTS: - this->real_gatts_event_handler_(ble_event->event_.gatts.gatts_event, ble_event->event_.gatts.gatts_if, - &ble_event->event_.gatts.gatts_param); + case BLEEvent::GATTS: { + esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event; + esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if; + esp_ble_gatts_cb_param_t *param = ble_event->event_.gatts.gatts_param; + ESP_LOGV(TAG, "gatts_event [esp_gatt_if: %d] - %d", gatts_if, event); + for (auto *gatts_handler : this->gatts_event_handlers_) { + gatts_handler->gatts_event_handler(event, gatts_if, param); + } break; - case BLEEvent::GATTC: - this->real_gattc_event_handler_(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if, - &ble_event->event_.gattc.gattc_param); + } + case BLEEvent::GATTC: { + esp_gattc_cb_event_t event = ble_event->event_.gattc.gattc_event; + esp_gatt_if_t gattc_if = ble_event->event_.gattc.gattc_if; + esp_ble_gattc_cb_param_t *param = ble_event->event_.gattc.gattc_param; + ESP_LOGV(TAG, "gattc_event [esp_gatt_if: %d] - %d", gattc_if, event); + for (auto *gattc_handler : this->gattc_event_handlers_) { + gattc_handler->gattc_event_handler(event, gattc_if, param); + } break; - case BLEEvent::GAP: - this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param); + } + case BLEEvent::GAP: { + esp_gap_ble_cb_event_t gap_event = ble_event->event_.gap.gap_event; + if (gap_event == ESP_GAP_BLE_SCAN_RESULT_EVT) { + // Use the new scan event handler - no memcpy! + for (auto *scan_handler : this->gap_scan_event_handlers_) { + scan_handler->gap_scan_event_handler(ble_event->scan_result()); + } + } else if (gap_event == ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT || + gap_event == ESP_GAP_BLE_SCAN_START_COMPLETE_EVT || + gap_event == ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT) { + // All three scan complete events have the same structure with just status + // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe + // This is verified at compile-time by static_assert checks in ble_event.h + // The struct already contains our copy of the status (copied in BLEEvent constructor) + ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler( + gap_event, reinterpret_cast(&ble_event->event_.gap.scan_complete)); + } + } break; + } default: break; } + // Destructor will clean up external allocations for GATTC/GATTS ble_event->~BLEEvent(); EVENT_ALLOCATOR.deallocate(ble_event, 1); ble_event = this->ble_events_.pop(); @@ -327,59 +362,55 @@ void ESP32BLE::loop() { } } -void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { +template void enqueue_ble_event(Args... args) { + if (global_ble->ble_events_.size() >= MAX_BLE_QUEUE_SIZE) { + ESP_LOGD(TAG, "Event queue full (%zu), dropping event", MAX_BLE_QUEUE_SIZE); + return; + } + BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); if (new_event == nullptr) { // Memory too fragmented to allocate new event. Can only drop it until memory comes back return; } - new (new_event) BLEEvent(event, param); + new (new_event) BLEEvent(args...); global_ble->ble_events_.push(new_event); } // NOLINT(clang-analyzer-unix.Malloc) -void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { - ESP_LOGV(TAG, "(BLE) gap_event_handler - %d", event); - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler(event, param); +// Explicit template instantiations for the friend function +template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *); +template void enqueue_ble_event(esp_gatts_cb_event_t, esp_gatt_if_t, esp_ble_gatts_cb_param_t *); +template void enqueue_ble_event(esp_gattc_cb_event_t, esp_gatt_if_t, esp_ble_gattc_cb_param_t *); + +void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + // Only queue the 4 GAP events we actually handle + case ESP_GAP_BLE_SCAN_RESULT_EVT: + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + enqueue_ble_event(event, param); + return; + + // Ignore these GAP events as they are not relevant for our use case + case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: + case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT: + return; + + default: + break; } + ESP_LOGW(TAG, "Ignoring unexpected GAP event type: %d", event); } void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { - BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); - if (new_event == nullptr) { - // Memory too fragmented to allocate new event. Can only drop it until memory comes back - return; - } - new (new_event) BLEEvent(event, gatts_if, param); - global_ble->ble_events_.push(new_event); -} // NOLINT(clang-analyzer-unix.Malloc) - -void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, - esp_ble_gatts_cb_param_t *param) { - ESP_LOGV(TAG, "(BLE) gatts_event [esp_gatt_if: %d] - %d", gatts_if, event); - for (auto *gatts_handler : this->gatts_event_handlers_) { - gatts_handler->gatts_event_handler(event, gatts_if, param); - } + enqueue_ble_event(event, gatts_if, param); } void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { - BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); - if (new_event == nullptr) { - // Memory too fragmented to allocate new event. Can only drop it until memory comes back - return; - } - new (new_event) BLEEvent(event, gattc_if, param); - global_ble->ble_events_.push(new_event); -} // NOLINT(clang-analyzer-unix.Malloc) - -void ESP32BLE::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) { - ESP_LOGV(TAG, "(BLE) gattc_event [esp_gatt_if: %d] - %d", gattc_if, event); - for (auto *gattc_handler : this->gattc_event_handlers_) { - gattc_handler->gattc_event_handler(event, gattc_if, param); - } + enqueue_ble_event(event, gattc_if, param); } float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; } diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 13ec3b6dd9..6508db1a00 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -2,6 +2,7 @@ #include "ble_advertising.h" #include "ble_uuid.h" +#include "ble_scan_result.h" #include @@ -22,6 +23,13 @@ namespace esphome { namespace esp32_ble { +// Maximum number of BLE scan results to buffer +#ifdef USE_PSRAM +static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 32; +#else +static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 20; +#endif + uint64_t ble_addr_to_uint64(const esp_bd_addr_t address); // NOLINTNEXTLINE(modernize-use-using) @@ -57,6 +65,11 @@ class GAPEventHandler { virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; }; +class GAPScanEventHandler { + public: + virtual void gap_scan_event_handler(const BLEScanResult &scan_result) = 0; +}; + class GATTcEventHandler { public: virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, @@ -101,6 +114,9 @@ class ESP32BLE : public Component { void advertising_register_raw_advertisement_callback(std::function &&callback); void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); } + void register_gap_scan_event_handler(GAPScanEventHandler *handler) { + this->gap_scan_event_handlers_.push_back(handler); + } void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); } void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); } void register_ble_status_event_handler(BLEStatusEventHandler *handler) { @@ -113,16 +129,16 @@ class ESP32BLE : public Component { static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); - void real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); - void real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); - void real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); - bool ble_setup_(); bool ble_dismantle_(); bool ble_pre_setup_(); void advertising_init_(); + private: + template friend void enqueue_ble_event(Args... args); + std::vector gap_event_handlers_; + std::vector gap_scan_event_handlers_; std::vector gattc_event_handlers_; std::vector gatts_event_handlers_; std::vector ble_status_event_handlers_; diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index 1cf63b2fab..f51095effd 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -2,92 +2,232 @@ #ifdef USE_ESP32 +#include // for offsetof #include #include #include #include +#include "ble_scan_result.h" + namespace esphome { namespace esp32_ble { + +// Compile-time verification that ESP-IDF scan complete events only contain a status field +// This ensures our reinterpret_cast in ble.cpp is safe +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF scan_param_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF scan_start_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF scan_stop_cmpl structure has unexpected size"); + +// Verify the status field is at offset 0 (first member) +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl.status) == + offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl), + "status must be first member of scan_param_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl.status) == + offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl), + "status must be first member of scan_start_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) == + offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl), + "status must be first member of scan_stop_cmpl"); + // Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop(). -// This class stores each event in a single type. +// This class stores each event with minimal memory usage. +// GAP events (99% of traffic) don't have the vector overhead. +// GATTC/GATTS events use heap allocation for their param and data. +// +// Event flow: +// 1. ESP-IDF BLE stack calls our static handlers in the BLE task context +// 2. The handlers create a BLEEvent instance, copying only the data we need +// 3. The event is pushed to a thread-safe queue +// 4. In the main loop(), events are popped from the queue and processed +// 5. The event destructor cleans up any external allocations +// +// Thread safety: +// - GAP events: We copy only the fields we need directly into the union +// - GATTC/GATTS events: We heap-allocate and copy the entire param struct, ensuring +// the data remains valid even after the BLE callback returns. The original +// param pointer from ESP-IDF is only valid during the callback. class BLEEvent { public: - BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { - this->event_.gap.gap_event = e; - memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t)); - this->type_ = GAP; - }; - - BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { - this->event_.gattc.gattc_event = e; - this->event_.gattc.gattc_if = i; - memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t)); - // Need to also make a copy of relevant event data. - switch (e) { - case ESP_GATTC_NOTIFY_EVT: - this->data.assign(p->notify.value, p->notify.value + p->notify.value_len); - this->event_.gattc.gattc_param.notify.value = this->data.data(); - break; - case ESP_GATTC_READ_CHAR_EVT: - case ESP_GATTC_READ_DESCR_EVT: - this->data.assign(p->read.value, p->read.value + p->read.value_len); - this->event_.gattc.gattc_param.read.value = this->data.data(); - break; - default: - break; - } - this->type_ = GATTC; - }; - - BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { - this->event_.gatts.gatts_event = e; - this->event_.gatts.gatts_if = i; - memcpy(&this->event_.gatts.gatts_param, p, sizeof(esp_ble_gatts_cb_param_t)); - // Need to also make a copy of relevant event data. - switch (e) { - case ESP_GATTS_WRITE_EVT: - this->data.assign(p->write.value, p->write.value + p->write.len); - this->event_.gatts.gatts_param.write.value = this->data.data(); - break; - default: - break; - } - this->type_ = GATTS; - }; - - union { - // NOLINTNEXTLINE(readability-identifier-naming) - struct gap_event { - esp_gap_ble_cb_event_t gap_event; - esp_ble_gap_cb_param_t gap_param; - } gap; - - // NOLINTNEXTLINE(readability-identifier-naming) - struct gattc_event { - esp_gattc_cb_event_t gattc_event; - esp_gatt_if_t gattc_if; - esp_ble_gattc_cb_param_t gattc_param; - } gattc; - - // NOLINTNEXTLINE(readability-identifier-naming) - struct gatts_event { - esp_gatts_cb_event_t gatts_event; - esp_gatt_if_t gatts_if; - esp_ble_gatts_cb_param_t gatts_param; - } gatts; - } event_; - - std::vector data{}; // NOLINTNEXTLINE(readability-identifier-naming) enum ble_event_t : uint8_t { GAP, GATTC, GATTS, - } type_; + }; + + // Constructor for GAP events - no external allocations needed + BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { + this->type_ = GAP; + this->event_.gap.gap_event = e; + + if (p == nullptr) { + return; // Invalid event, but we can't log in header file + } + + // Only copy the data we actually use for each GAP event type + switch (e) { + case ESP_GAP_BLE_SCAN_RESULT_EVT: + // Copy only the fields we use from scan results + memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t)); + this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type; + this->event_.gap.scan_result.rssi = p->scan_rst.rssi; + this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len; + this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len; + this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt; + memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv, + ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX); + break; + + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + this->event_.gap.scan_complete.status = p->scan_param_cmpl.status; + break; + + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + this->event_.gap.scan_complete.status = p->scan_start_cmpl.status; + break; + + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status; + break; + + default: + // We only handle 4 GAP event types, others are dropped + break; + } + } + + // Constructor for GATTC events - uses heap allocation + // Creates a copy of the param struct since the original is only valid during the callback + BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + this->type_ = GATTC; + this->event_.gattc.gattc_event = e; + this->event_.gattc.gattc_if = i; + + if (p == nullptr) { + this->event_.gattc.gattc_param = nullptr; + this->event_.gattc.data = nullptr; + return; // Invalid event, but we can't log in header file + } + + // Heap-allocate param and data + // Heap allocation is used because GATTC/GATTS events are rare (<1% of events) + // while GAP events (99%) are stored inline to minimize memory usage + this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p); + + // Copy data for events that need it + switch (e) { + case ESP_GATTC_NOTIFY_EVT: + this->event_.gattc.data = new std::vector(p->notify.value, p->notify.value + p->notify.value_len); + this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data(); + break; + case ESP_GATTC_READ_CHAR_EVT: + case ESP_GATTC_READ_DESCR_EVT: + this->event_.gattc.data = new std::vector(p->read.value, p->read.value + p->read.value_len); + this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data(); + break; + default: + this->event_.gattc.data = nullptr; + break; + } + } + + // Constructor for GATTS events - uses heap allocation + // Creates a copy of the param struct since the original is only valid during the callback + BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { + this->type_ = GATTS; + this->event_.gatts.gatts_event = e; + this->event_.gatts.gatts_if = i; + + if (p == nullptr) { + this->event_.gatts.gatts_param = nullptr; + this->event_.gatts.data = nullptr; + return; // Invalid event, but we can't log in header file + } + + // Heap-allocate param and data + // Heap allocation is used because GATTC/GATTS events are rare (<1% of events) + // while GAP events (99%) are stored inline to minimize memory usage + this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p); + + // Copy data for events that need it + switch (e) { + case ESP_GATTS_WRITE_EVT: + this->event_.gatts.data = new std::vector(p->write.value, p->write.value + p->write.len); + this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data(); + break; + default: + this->event_.gatts.data = nullptr; + break; + } + } + + // Destructor to clean up heap allocations + ~BLEEvent() { + switch (this->type_) { + case GATTC: + delete this->event_.gattc.gattc_param; + delete this->event_.gattc.data; + break; + case GATTS: + delete this->event_.gatts.gatts_param; + delete this->event_.gatts.data; + break; + default: + break; + } + } + + // Disable copy to prevent double-delete + BLEEvent(const BLEEvent &) = delete; + BLEEvent &operator=(const BLEEvent &) = delete; + + union { + // NOLINTNEXTLINE(readability-identifier-naming) + struct gap_event { + esp_gap_ble_cb_event_t gap_event; + union { + BLEScanResult scan_result; // 73 bytes + // This matches ESP-IDF's scan complete event structures + // All three (scan_param_cmpl, scan_start_cmpl, scan_stop_cmpl) have identical layout + struct { + esp_bt_status_t status; + } scan_complete; // 1 byte + }; + } gap; // 80 bytes total + + // NOLINTNEXTLINE(readability-identifier-naming) + struct gattc_event { + esp_gattc_cb_event_t gattc_event; + esp_gatt_if_t gattc_if; + esp_ble_gattc_cb_param_t *gattc_param; // Heap-allocated + std::vector *data; // Heap-allocated + } gattc; // 16 bytes (pointers only) + + // NOLINTNEXTLINE(readability-identifier-naming) + struct gatts_event { + esp_gatts_cb_event_t gatts_event; + esp_gatt_if_t gatts_if; + esp_ble_gatts_cb_param_t *gatts_param; // Heap-allocated + std::vector *data; // Heap-allocated + } gatts; // 16 bytes (pointers only) + } event_; // 80 bytes + + ble_event_t type_; + + // Helper methods to access event data + ble_event_t type() const { return type_; } + esp_gap_ble_cb_event_t gap_event_type() const { return event_.gap.gap_event; } + const BLEScanResult &scan_result() const { return event_.gap.scan_result; } + esp_bt_status_t scan_complete_status() const { return event_.gap.scan_complete.status; } }; +// BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding) + } // namespace esp32_ble } // namespace esphome diff --git a/esphome/components/esp32_ble/ble_scan_result.h b/esphome/components/esp32_ble/ble_scan_result.h new file mode 100644 index 0000000000..49b0d5523d --- /dev/null +++ b/esphome/components/esp32_ble/ble_scan_result.h @@ -0,0 +1,24 @@ +#pragma once + +#ifdef USE_ESP32 + +#include + +namespace esphome { +namespace esp32_ble { + +// Structure for BLE scan results - only fields we actually use +struct __attribute__((packed)) BLEScanResult { + esp_bd_addr_t bda; + uint8_t ble_addr_type; + int8_t rssi; + uint8_t ble_adv[ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX]; + uint8_t adv_data_len; + uint8_t scan_rsp_len; + uint8_t search_evt; +}; // ~73 bytes vs ~400 bytes for full esp_ble_gap_cb_param_t + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h index c98477e121..f69878bf6e 100644 --- a/esphome/components/esp32_ble/queue.h +++ b/esphome/components/esp32_ble/queue.h @@ -45,6 +45,17 @@ template class Queue { return element; } + size_t size() const { + // Lock-free size check. While std::queue::size() is not thread-safe, we intentionally + // avoid locking here to prevent blocking the BLE callback thread. The size is only + // used to decide whether to drop incoming events when the queue is near capacity. + // With a queue limit of 40-64 events and normal processing, dropping events should + // be extremely rare. When it does approach capacity, being off by 1-2 events is + // acceptable to avoid blocking the BLE stack's time-sensitive callbacks. + // Trade-off: We prefer occasional dropped events over potential BLE stack delays. + return q_.size(); + } + protected: std::queue q_; SemaphoreHandle_t m_; diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 61eed1c029..2242d709a4 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -268,6 +268,7 @@ async def to_code(config): parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) cg.add(parent.register_gap_event_handler(var)) + cg.add(parent.register_gap_scan_event_handler(var)) cg.add(parent.register_gattc_event_handler(var)) cg.add(parent.register_ble_status_event_handler(var)) cg.add(var.set_parent(parent)) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 6d60f1638c..ab3efc3ad3 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -50,9 +50,8 @@ void ESP32BLETracker::setup() { ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE"); return; } - ExternalRAMAllocator allocator( - ExternalRAMAllocator::ALLOW_FAILURE); - this->scan_result_buffer_ = allocator.allocate(ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE); + RAMAllocator allocator; + this->scan_result_buffer_ = allocator.allocate(SCAN_RESULT_BUFFER_SIZE); if (this->scan_result_buffer_ == nullptr) { ESP_LOGE(TAG, "Could not allocate buffer for BLE Tracker!"); @@ -124,7 +123,7 @@ void ESP32BLETracker::loop() { this->scan_result_index_ && // if it looks like we have a scan result we will take the lock xSemaphoreTake(this->scan_result_lock_, 0)) { uint32_t index = this->scan_result_index_; - if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { + if (index >= SCAN_RESULT_BUFFER_SIZE) { ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); } @@ -370,9 +369,6 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() { void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { - case ESP_GAP_BLE_SCAN_RESULT_EVT: - this->gap_scan_result_(param->scan_rst); - break; case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: this->gap_scan_set_param_complete_(param->scan_param_cmpl); break; @@ -385,11 +381,42 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga default: break; } + // Forward all events to clients (scan results are handled separately via gap_scan_event_handler) for (auto *client : this->clients_) { client->gap_event_handler(event, param); } } +void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) { + ESP_LOGV(TAG, "gap_scan_result - event %d", scan_result.search_evt); + + if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { + if (xSemaphoreTake(this->scan_result_lock_, 0)) { + if (this->scan_result_index_ < SCAN_RESULT_BUFFER_SIZE) { + // Store BLEScanResult directly in our buffer + this->scan_result_buffer_[this->scan_result_index_++] = scan_result; + } + xSemaphoreGive(this->scan_result_lock_); + } + } else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) { + // Scan finished on its own + if (this->scanner_state_ != ScannerState::RUNNING) { + if (this->scanner_state_ == ScannerState::STOPPING) { + ESP_LOGE(TAG, "Scan was not running when scan completed."); + } else if (this->scanner_state_ == ScannerState::STARTING) { + ESP_LOGE(TAG, "Scan was not started when scan completed."); + } else if (this->scanner_state_ == ScannerState::FAILED) { + ESP_LOGE(TAG, "Scan was in failed state when scan completed."); + } else if (this->scanner_state_ == ScannerState::IDLE) { + ESP_LOGE(TAG, "Scan was idle when scan completed."); + } else if (this->scanner_state_ == ScannerState::STOPPED) { + ESP_LOGE(TAG, "Scan was stopped when scan completed."); + } + } + this->set_scanner_state_(ScannerState::STOPPED); + } +} + void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m) { ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status); if (param.status == ESP_BT_STATUS_DONE) { @@ -444,34 +471,6 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_ this->set_scanner_state_(ScannerState::STOPPED); } -void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { - ESP_LOGV(TAG, "gap_scan_result - event %d", param.search_evt); - if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { - if (xSemaphoreTake(this->scan_result_lock_, 0)) { - if (this->scan_result_index_ < ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { - this->scan_result_buffer_[this->scan_result_index_++] = param; - } - xSemaphoreGive(this->scan_result_lock_); - } - } else if (param.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) { - // Scan finished on its own - if (this->scanner_state_ != ScannerState::RUNNING) { - if (this->scanner_state_ == ScannerState::STOPPING) { - ESP_LOGE(TAG, "Scan was not running when scan completed."); - } else if (this->scanner_state_ == ScannerState::STARTING) { - ESP_LOGE(TAG, "Scan was not started when scan completed."); - } else if (this->scanner_state_ == ScannerState::FAILED) { - ESP_LOGE(TAG, "Scan was in failed state when scan completed."); - } else if (this->scanner_state_ == ScannerState::IDLE) { - ESP_LOGE(TAG, "Scan was idle when scan completed."); - } else if (this->scanner_state_ == ScannerState::STOPPED) { - ESP_LOGE(TAG, "Scan was stopped when scan completed."); - } - } - this->set_scanner_state_(ScannerState::STOPPED); - } -} - void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { for (auto *client : this->clients_) { @@ -494,13 +493,15 @@ optional ESPBLEiBeacon::from_manufacturer_data(const ServiceData return ESPBLEiBeacon(data.data.data()); } -void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { - this->scan_result_ = param; +void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) { for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++) - this->address_[i] = param.bda[i]; - this->address_type_ = param.ble_addr_type; - this->rssi_ = param.rssi; - this->parse_adv_(param); + this->address_[i] = scan_result.bda[i]; + this->address_type_ = static_cast(scan_result.ble_addr_type); + this->rssi_ = scan_result.rssi; + + // Parse advertisement data directly + uint8_t total_len = scan_result.adv_data_len + scan_result.scan_rsp_len; + this->parse_adv_(scan_result.ble_adv, total_len); #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE ESP_LOGVV(TAG, "Parse Result:"); @@ -558,13 +559,13 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str()); } - ESP_LOGVV(TAG, " Adv data: %s", format_hex_pretty(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str()); + ESP_LOGVV(TAG, " Adv data: %s", + format_hex_pretty(scan_result.ble_adv, scan_result.adv_data_len + scan_result.scan_rsp_len).c_str()); #endif } -void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { + +void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) { size_t offset = 0; - const uint8_t *payload = param.ble_adv; - uint8_t len = param.adv_data_len + param.scan_rsp_len; while (offset + 2 < len) { const uint8_t field_length = payload[offset++]; // First byte is length of adv record diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index eea73a7d26..33c0caaa87 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -62,7 +62,7 @@ class ESPBLEiBeacon { class ESPBTDevice { public: - void parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); + void parse_scan_rst(const BLEScanResult &scan_result); std::string address_str() const; @@ -84,8 +84,6 @@ class ESPBTDevice { const std::vector &get_service_datas() const { return service_datas_; } - const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &get_scan_result() const { return scan_result_; } - bool resolve_irk(const uint8_t *irk) const; optional get_ibeacon() const { @@ -98,7 +96,7 @@ class ESPBTDevice { } protected: - void parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); + void parse_adv_(const uint8_t *payload, uint8_t len); esp_bd_addr_t address_{ 0, @@ -112,7 +110,6 @@ class ESPBTDevice { std::vector service_uuids_{}; std::vector manufacturer_datas_{}; std::vector service_datas_{}; - esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_{}; }; class ESP32BLETracker; @@ -121,9 +118,7 @@ class ESPBTDeviceListener { public: virtual void on_scan_end() {} virtual bool parse_device(const ESPBTDevice &device) = 0; - virtual bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { - return false; - }; + virtual bool parse_devices(const BLEScanResult *scan_results, size_t count) { return false; }; virtual AdvertisementParserType get_advertisement_parser_type() { return AdvertisementParserType::PARSED_ADVERTISEMENTS; }; @@ -210,6 +205,7 @@ class ESPBTClient : public ESPBTDeviceListener { class ESP32BLETracker : public Component, public GAPEventHandler, + public GAPScanEventHandler, public GATTcEventHandler, public BLEStatusEventHandler, public Parented { @@ -240,6 +236,7 @@ class ESP32BLETracker : public Component, void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; + void gap_scan_event_handler(const BLEScanResult &scan_result) override; void ble_before_disabled_event_handler() override; void add_scanner_state_callback(std::function &&callback) { @@ -287,12 +284,7 @@ class ESP32BLETracker : public Component, bool parse_advertisements_{false}; SemaphoreHandle_t scan_result_lock_; size_t scan_result_index_{0}; -#ifdef USE_PSRAM - const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 32; -#else - const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 20; -#endif // USE_PSRAM - esp_ble_gap_cb_param_t::ble_scan_result_evt_param *scan_result_buffer_; + BLEScanResult *scan_result_buffer_; esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; int connecting_{0}; diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 59a3398ce8..28a66b23b7 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -116,7 +116,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr if (this->baud_rate_ > 0) { this->write_msg_(this->tx_buffer_ + msg_start); } - this->call_log_callbacks_(level, tag, this->tx_buffer_ + msg_start); + this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start); global_recursion_guard_ = false; } @@ -129,19 +129,6 @@ inline int Logger::level_for(const char *tag) { return this->current_level_; } -void HOT Logger::call_log_callbacks_(int level, const char *tag, const char *msg) { -#ifdef USE_ESP32 - // Suppress network-logging if memory constrained - // In some configurations (eg BLE enabled) there may be some transient - // memory exhaustion, and trying to log when OOM can lead to a crash. Skipping - // here usually allows the stack to recover instead. - // See issue #1234 for analysis. - if (xPortGetFreeHeapSize() < 2048) - return; -#endif - this->log_callback_.call(level, tag, msg); -} - Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size) { // add 1 to buffer size for null terminator this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT @@ -189,7 +176,7 @@ void Logger::loop() { this->tx_buffer_size_); this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); this->tx_buffer_[this->tx_buffer_at_] = '\0'; - this->call_log_callbacks_(message->level, message->tag, this->tx_buffer_); + this->log_callback_.call(message->level, message->tag, this->tx_buffer_); // At this point all the data we need from message has been transferred to the tx_buffer // so we can release the message to allow other tasks to use it as soon as possible. this->log_buffer_->release_message_main_loop(received_token); diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 6030d9e8f2..9f09208b66 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -156,7 +156,6 @@ class Logger : public Component { #endif protected: - void call_log_callbacks_(int level, const char *tag, const char *msg); void write_msg_(const char *msg); // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator @@ -191,7 +190,7 @@ class Logger : public Component { if (this->baud_rate_ > 0) { this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console } - this->call_log_callbacks_(level, tag, this->tx_buffer_); + this->log_callback_.call(level, tag, this->tx_buffer_); } // Write the body of the log message to the buffer From f82ac34784b9e43eb0da076f49d936bf74e9495c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 10:42:01 -0500 Subject: [PATCH 025/293] Bump aioesphomeapi from 32.2.1 to 32.2.3 (#9091) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d4bd0b7543..682f9dbe60 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.8.1 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==32.2.1 +aioesphomeapi==32.2.3 zeroconf==0.147.0 puremagic==1.29 ruamel.yaml==0.18.14 # dashboard_import From 59f69ac5caeb3bef5296bbd1072661a424497ed2 Mon Sep 17 00:00:00 2001 From: dhewg Date: Sun, 15 Jun 2025 20:16:33 +0200 Subject: [PATCH 026/293] [fan] fix initial FanCall to properly set speed (#8277) --- esphome/components/fan/fan.cpp | 55 ++++--- .../fixtures/host_mode_fan_preset.yaml | 34 ++++ .../integration/test_host_mode_fan_preset.py | 152 ++++++++++++++++++ 3 files changed, 218 insertions(+), 23 deletions(-) create mode 100644 tests/integration/fixtures/host_mode_fan_preset.yaml create mode 100644 tests/integration/test_host_mode_fan_preset.py diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 87bf4939a0..25f710f893 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -41,39 +41,48 @@ void FanCall::perform() { void FanCall::validate_() { auto traits = this->parent_.get_traits(); - if (this->speed_.has_value()) + if (this->speed_.has_value()) { this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count()); - if (this->binary_state_.has_value() && *this->binary_state_) { - // when turning on, if neither current nor new speed available, set speed to 100% - if (traits.supports_speed() && !this->parent_.state && this->parent_.speed == 0 && !this->speed_.has_value()) { - this->speed_ = traits.supported_speed_count(); - } - } - - if (this->oscillating_.has_value() && !traits.supports_oscillation()) { - ESP_LOGW(TAG, "'%s' - This fan does not support oscillation!", this->parent_.get_name().c_str()); - this->oscillating_.reset(); - } - - if (this->speed_.has_value() && !traits.supports_speed()) { - ESP_LOGW(TAG, "'%s' - This fan does not support speeds!", this->parent_.get_name().c_str()); - this->speed_.reset(); - } - - if (this->direction_.has_value() && !traits.supports_direction()) { - ESP_LOGW(TAG, "'%s' - This fan does not support directions!", this->parent_.get_name().c_str()); - this->direction_.reset(); + // https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes + // "Manually setting a speed must disable any set preset mode" + this->preset_mode_.clear(); } if (!this->preset_mode_.empty()) { const auto &preset_modes = traits.supported_preset_modes(); if (preset_modes.find(this->preset_mode_) == preset_modes.end()) { - ESP_LOGW(TAG, "'%s' - This fan does not support preset mode '%s'!", this->parent_.get_name().c_str(), - this->preset_mode_.c_str()); + ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str()); this->preset_mode_.clear(); } } + + // when turning on... + if (!this->parent_.state && this->binary_state_.has_value() && + *this->binary_state_ + // ..,and no preset mode will be active... + && this->preset_mode_.empty() && + this->parent_.preset_mode.empty() + // ...and neither current nor new speed is available... + && traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) { + // ...set speed to 100% + this->speed_ = traits.supported_speed_count(); + } + + if (this->oscillating_.has_value() && !traits.supports_oscillation()) { + ESP_LOGW(TAG, "%s: Oscillation not supported", this->parent_.get_name().c_str()); + this->oscillating_.reset(); + } + + if (this->speed_.has_value() && !traits.supports_speed()) { + ESP_LOGW(TAG, "%s: Speed control not supported", this->parent_.get_name().c_str()); + this->speed_.reset(); + } + + if (this->direction_.has_value() && !traits.supports_direction()) { + ESP_LOGW(TAG, "%s: Direction control not supported", this->parent_.get_name().c_str()); + this->direction_.reset(); + } } FanCall FanRestoreState::to_call(Fan &fan) { diff --git a/tests/integration/fixtures/host_mode_fan_preset.yaml b/tests/integration/fixtures/host_mode_fan_preset.yaml new file mode 100644 index 0000000000..003f4a7760 --- /dev/null +++ b/tests/integration/fixtures/host_mode_fan_preset.yaml @@ -0,0 +1,34 @@ +esphome: + name: host-test + +host: + +api: + +logger: + +# Test fan with preset modes and speed settings +fan: + - platform: template + name: "Test Fan with Presets" + id: test_fan_presets + speed_count: 5 + preset_modes: + - "Eco" + - "Sleep" + - "Turbo" + has_oscillating: true + has_direction: true + + - platform: template + name: "Test Fan Simple" + id: test_fan_simple + speed_count: 3 + has_oscillating: false + has_direction: false + + - platform: template + name: "Test Fan No Speed" + id: test_fan_no_speed + has_oscillating: true + has_direction: false diff --git a/tests/integration/test_host_mode_fan_preset.py b/tests/integration/test_host_mode_fan_preset.py new file mode 100644 index 0000000000..1d956a7290 --- /dev/null +++ b/tests/integration/test_host_mode_fan_preset.py @@ -0,0 +1,152 @@ +"""Integration test for fan preset mode behavior.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import FanInfo, FanState +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_host_mode_fan_preset( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test fan preset mode behavior according to Home Assistant guidelines.""" + # Write, compile and run the ESPHome device, then connect to API + async with run_compiled(yaml_config), api_client_connected() as client: + # Get all fan entities + entities = await client.list_entities_services() + fans: list[FanInfo] = [] + for entity_list in entities: + for entity in entity_list: + if isinstance(entity, FanInfo): + fans.append(entity) + + # Create a map of fan names to entity info + fan_map = {fan.name: fan for fan in fans} + + # Verify we have our test fans + assert "Test Fan with Presets" in fan_map + assert "Test Fan Simple" in fan_map + assert "Test Fan No Speed" in fan_map + + # Get fan with presets + fan_presets = fan_map["Test Fan with Presets"] + assert fan_presets.supports_speed is True + assert fan_presets.supported_speed_count == 5 + assert fan_presets.supports_oscillation is True + assert fan_presets.supports_direction is True + assert set(fan_presets.supported_preset_modes) == {"Eco", "Sleep", "Turbo"} + + # Subscribe to states + states: dict[int, FanState] = {} + state_event = asyncio.Event() + + def on_state(state: FanState) -> None: + if isinstance(state, FanState): + states[state.key] = state + state_event.set() + + client.subscribe_states(on_state) + + # Test 1: Turn on fan without speed or preset - should set speed to 100% + state_event.clear() + client.fan_command( + key=fan_presets.key, + state=True, + ) + await asyncio.wait_for(state_event.wait(), timeout=2.0) + + fan_state = states[fan_presets.key] + assert fan_state.state is True + assert fan_state.speed_level == 5 # Should be max speed (100%) + assert fan_state.preset_mode == "" + + # Turn off + state_event.clear() + client.fan_command( + key=fan_presets.key, + state=False, + ) + await asyncio.wait_for(state_event.wait(), timeout=2.0) + + # Test 2: Turn on fan with preset mode - should NOT set speed to 100% + state_event.clear() + client.fan_command( + key=fan_presets.key, + state=True, + preset_mode="Eco", + ) + await asyncio.wait_for(state_event.wait(), timeout=2.0) + + fan_state = states[fan_presets.key] + assert fan_state.state is True + assert fan_state.preset_mode == "Eco" + # Speed should be whatever the preset sets, not forced to 100% + + # Test 3: Setting speed should clear preset mode + state_event.clear() + client.fan_command( + key=fan_presets.key, + speed_level=3, + ) + await asyncio.wait_for(state_event.wait(), timeout=2.0) + + fan_state = states[fan_presets.key] + assert fan_state.state is True + assert fan_state.speed_level == 3 + assert fan_state.preset_mode == "" # Preset mode should be cleared + + # Test 4: Setting preset mode should work when fan is already on + state_event.clear() + client.fan_command( + key=fan_presets.key, + preset_mode="Sleep", + ) + await asyncio.wait_for(state_event.wait(), timeout=2.0) + + fan_state = states[fan_presets.key] + assert fan_state.state is True + assert fan_state.preset_mode == "Sleep" + + # Turn off + state_event.clear() + client.fan_command( + key=fan_presets.key, + state=False, + ) + await asyncio.wait_for(state_event.wait(), timeout=2.0) + + # Test 5: Turn on fan with specific speed + state_event.clear() + client.fan_command( + key=fan_presets.key, + state=True, + speed_level=2, + ) + await asyncio.wait_for(state_event.wait(), timeout=2.0) + + fan_state = states[fan_presets.key] + assert fan_state.state is True + assert fan_state.speed_level == 2 + assert fan_state.preset_mode == "" + + # Test 6: Test fan with no speed support + fan_no_speed = fan_map["Test Fan No Speed"] + assert fan_no_speed.supports_speed is False + + state_event.clear() + client.fan_command( + key=fan_no_speed.key, + state=True, + ) + await asyncio.wait_for(state_event.wait(), timeout=2.0) + + fan_state = states[fan_no_speed.key] + assert fan_state.state is True + # No speed should be set for fans that don't support speed From 61a558a062e9df0c9d412ab1fa7a5c4f0c34bba5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Jun 2025 15:53:45 -0500 Subject: [PATCH 027/293] Implement a lock free ring buffer for BLEScanResult to avoid drops (#9087) --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 88 ++++++++++++------- .../esp32_ble_tracker/esp32_ble_tracker.h | 14 ++- 2 files changed, 69 insertions(+), 33 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index ab3efc3ad3..c5906779f1 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -51,15 +51,14 @@ void ESP32BLETracker::setup() { return; } RAMAllocator allocator; - this->scan_result_buffer_ = allocator.allocate(SCAN_RESULT_BUFFER_SIZE); + this->scan_ring_buffer_ = allocator.allocate(SCAN_RESULT_BUFFER_SIZE); - if (this->scan_result_buffer_ == nullptr) { - ESP_LOGE(TAG, "Could not allocate buffer for BLE Tracker!"); + if (this->scan_ring_buffer_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate ring buffer for BLE Tracker!"); this->mark_failed(); } global_esp32_ble_tracker = this; - this->scan_result_lock_ = xSemaphoreCreateMutex(); #ifdef USE_OTA ota::get_global_ota_callback()->add_on_state_callback( @@ -119,27 +118,31 @@ void ESP32BLETracker::loop() { } bool promote_to_connecting = discovered && !searching && !connecting; - if (this->scanner_state_ == ScannerState::RUNNING && - this->scan_result_index_ && // if it looks like we have a scan result we will take the lock - xSemaphoreTake(this->scan_result_lock_, 0)) { - uint32_t index = this->scan_result_index_; - if (index >= SCAN_RESULT_BUFFER_SIZE) { - ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); - } + // Process scan results from lock-free SPSC ring buffer + // Consumer side: This runs in the main loop thread + if (this->scanner_state_ == ScannerState::RUNNING) { + // Load our own index with relaxed ordering (we're the only writer) + size_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed); - if (this->raw_advertisements_) { - for (auto *listener : this->listeners_) { - listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_); - } - for (auto *client : this->clients_) { - client->parse_devices(this->scan_result_buffer_, this->scan_result_index_); - } - } + // Load producer's index with acquire to see their latest writes + size_t write_idx = this->ring_write_index_.load(std::memory_order_acquire); - if (this->parse_advertisements_) { - for (size_t i = 0; i < index; i++) { + while (read_idx != write_idx) { + // Process one result at a time directly from ring buffer + BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx]; + + if (this->raw_advertisements_) { + for (auto *listener : this->listeners_) { + listener->parse_devices(&scan_result, 1); + } + for (auto *client : this->clients_) { + client->parse_devices(&scan_result, 1); + } + } + + if (this->parse_advertisements_) { ESPBTDevice device; - device.parse_scan_rst(this->scan_result_buffer_[i]); + device.parse_scan_rst(scan_result); bool found = false; for (auto *listener : this->listeners_) { @@ -160,9 +163,19 @@ void ESP32BLETracker::loop() { this->print_bt_device_info(device); } } + + // Move to next entry in ring buffer + read_idx = (read_idx + 1) % SCAN_RESULT_BUFFER_SIZE; + + // Store with release to ensure reads complete before index update + this->ring_read_index_.store(read_idx, std::memory_order_release); + } + + // Log dropped results periodically + size_t dropped = this->scan_results_dropped_.exchange(0, std::memory_order_relaxed); + if (dropped > 0) { + ESP_LOGW(TAG, "Dropped %zu BLE scan results due to buffer overflow", dropped); } - this->scan_result_index_ = 0; - xSemaphoreGive(this->scan_result_lock_); } if (this->scanner_state_ == ScannerState::STOPPED) { this->end_of_scan_(); // Change state to IDLE @@ -391,12 +404,27 @@ void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) { ESP_LOGV(TAG, "gap_scan_result - event %d", scan_result.search_evt); if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { - if (xSemaphoreTake(this->scan_result_lock_, 0)) { - if (this->scan_result_index_ < SCAN_RESULT_BUFFER_SIZE) { - // Store BLEScanResult directly in our buffer - this->scan_result_buffer_[this->scan_result_index_++] = scan_result; - } - xSemaphoreGive(this->scan_result_lock_); + // Lock-free SPSC ring buffer write (Producer side) + // This runs in the ESP-IDF Bluetooth stack callback thread + // IMPORTANT: Only this thread writes to ring_write_index_ + + // Load our own index with relaxed ordering (we're the only writer) + size_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed); + size_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE; + + // Load consumer's index with acquire to see their latest updates + size_t read_idx = this->ring_read_index_.load(std::memory_order_acquire); + + // Check if buffer is full + if (next_write_idx != read_idx) { + // Write to ring buffer + this->scan_ring_buffer_[write_idx] = scan_result; + + // Store with release to ensure the write is visible before index update + this->ring_write_index_.store(next_write_idx, std::memory_order_release); + } else { + // Buffer full, track dropped results + this->scan_results_dropped_.fetch_add(1, std::memory_order_relaxed); } } else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) { // Scan finished on its own diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 33c0caaa87..16a100fb47 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -6,6 +6,7 @@ #include "esphome/core/helpers.h" #include +#include #include #include @@ -282,9 +283,16 @@ class ESP32BLETracker : public Component, bool ble_was_disabled_{true}; bool raw_advertisements_{false}; bool parse_advertisements_{false}; - SemaphoreHandle_t scan_result_lock_; - size_t scan_result_index_{0}; - BLEScanResult *scan_result_buffer_; + + // Lock-free Single-Producer Single-Consumer (SPSC) ring buffer for scan results + // Producer: ESP-IDF Bluetooth stack callback (gap_scan_event_handler) + // Consumer: ESPHome main loop (loop() method) + // This design ensures zero blocking in the BT callback and prevents scan result loss + BLEScanResult *scan_ring_buffer_; + std::atomic ring_write_index_{0}; // Written only by BT callback (producer) + std::atomic ring_read_index_{0}; // Written only by main loop (consumer) + std::atomic scan_results_dropped_{0}; // Tracks buffer overflow events + esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; int connecting_{0}; From fcce4a8be6dbfd01a84c1827a45ecda3a339ce15 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Jun 2025 16:16:46 -0500 Subject: [PATCH 028/293] Make BLE queue lock free (#9088) --- esphome/components/esp32_ble/ble.cpp | 26 +++++++-- esphome/components/esp32_ble/ble.h | 5 +- esphome/components/esp32_ble/queue.h | 83 ++++++++++++++++------------ 3 files changed, 73 insertions(+), 41 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index ed74d59ef2..8adef79d2f 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -23,9 +23,6 @@ namespace esp32_ble { static const char *const TAG = "esp32_ble"; -// Maximum size of the BLE event queue -static constexpr size_t MAX_BLE_QUEUE_SIZE = SCAN_RESULT_BUFFER_SIZE * 2; - static RAMAllocator EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) RAMAllocator::ALLOW_FAILURE | RAMAllocator::ALLOC_INTERNAL); @@ -360,21 +357,38 @@ void ESP32BLE::loop() { if (this->advertising_ != nullptr) { this->advertising_->loop(); } + + // Log dropped events periodically + size_t dropped = this->ble_events_.get_and_reset_dropped_count(); + if (dropped > 0) { + ESP_LOGW(TAG, "Dropped %zu BLE events due to buffer overflow", dropped); + } } template void enqueue_ble_event(Args... args) { - if (global_ble->ble_events_.size() >= MAX_BLE_QUEUE_SIZE) { - ESP_LOGD(TAG, "Event queue full (%zu), dropping event", MAX_BLE_QUEUE_SIZE); + // Check if queue is full before allocating + if (global_ble->ble_events_.full()) { + // Queue is full, drop the event + global_ble->ble_events_.increment_dropped_count(); return; } BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); if (new_event == nullptr) { // Memory too fragmented to allocate new event. Can only drop it until memory comes back + global_ble->ble_events_.increment_dropped_count(); return; } new (new_event) BLEEvent(args...); - global_ble->ble_events_.push(new_event); + + // Push the event - since we're the only producer and we checked full() above, + // this should always succeed unless we have a bug + if (!global_ble->ble_events_.push(new_event)) { + // This should not happen in SPSC queue with single producer + ESP_LOGE(TAG, "BLE queue push failed unexpectedly"); + new_event->~BLEEvent(); + EVENT_ALLOCATOR.deallocate(new_event, 1); + } } // NOLINT(clang-analyzer-unix.Malloc) // Explicit template instantiations for the friend function diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 6508db1a00..58c064a2ef 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -30,6 +30,9 @@ static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 32; static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 20; #endif +// Maximum size of the BLE event queue - must be power of 2 for lock-free queue +static constexpr size_t MAX_BLE_QUEUE_SIZE = 64; + uint64_t ble_addr_to_uint64(const esp_bd_addr_t address); // NOLINTNEXTLINE(modernize-use-using) @@ -144,7 +147,7 @@ class ESP32BLE : public Component { std::vector ble_status_event_handlers_; BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; - Queue ble_events_; + LockFreeQueue ble_events_; BLEAdvertising *advertising_{}; esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; uint32_t advertising_cycle_time_{}; diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h index f69878bf6e..56d2efd18b 100644 --- a/esphome/components/esp32_ble/queue.h +++ b/esphome/components/esp32_ble/queue.h @@ -2,63 +2,78 @@ #ifdef USE_ESP32 -#include -#include - -#include -#include +#include +#include /* * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather - * than trying to deal with various locking strategies, all incoming GAP and GATT - * events will simply be placed on a semaphore guarded queue. The next time the - * component runs loop(), these events are popped off the queue and handed at - * this safer time. + * than using mutex-based locking, this lock-free queue allows the BLE + * task to enqueue events without blocking. The main loop() then processes + * these events at a safer time. + * + * This is a Single-Producer Single-Consumer (SPSC) lock-free ring buffer. + * The BLE task is the only producer, and the main loop() is the only consumer. */ namespace esphome { namespace esp32_ble { -template class Queue { +template class LockFreeQueue { public: - Queue() { m_ = xSemaphoreCreateMutex(); } + LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {} - void push(T *element) { + bool push(T *element) { if (element == nullptr) - return; - // It is not called from main loop. Thus it won't block main thread. - xSemaphoreTake(m_, portMAX_DELAY); - q_.push(element); - xSemaphoreGive(m_); + return false; + + size_t current_tail = tail_.load(std::memory_order_relaxed); + size_t next_tail = (current_tail + 1) % SIZE; + + if (next_tail == head_.load(std::memory_order_acquire)) { + // Buffer full + dropped_count_.fetch_add(1, std::memory_order_relaxed); + return false; + } + + buffer_[current_tail] = element; + tail_.store(next_tail, std::memory_order_release); + return true; } T *pop() { - T *element = nullptr; + size_t current_head = head_.load(std::memory_order_relaxed); - if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) { - if (!q_.empty()) { - element = q_.front(); - q_.pop(); - } - xSemaphoreGive(m_); + if (current_head == tail_.load(std::memory_order_acquire)) { + return nullptr; // Empty } + + T *element = buffer_[current_head]; + head_.store((current_head + 1) % SIZE, std::memory_order_release); return element; } size_t size() const { - // Lock-free size check. While std::queue::size() is not thread-safe, we intentionally - // avoid locking here to prevent blocking the BLE callback thread. The size is only - // used to decide whether to drop incoming events when the queue is near capacity. - // With a queue limit of 40-64 events and normal processing, dropping events should - // be extremely rare. When it does approach capacity, being off by 1-2 events is - // acceptable to avoid blocking the BLE stack's time-sensitive callbacks. - // Trade-off: We prefer occasional dropped events over potential BLE stack delays. - return q_.size(); + size_t tail = tail_.load(std::memory_order_acquire); + size_t head = head_.load(std::memory_order_acquire); + return (tail - head + SIZE) % SIZE; + } + + size_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } + + void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); } + + bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); } + + bool full() const { + size_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; + return next_tail == head_.load(std::memory_order_acquire); } protected: - std::queue q_; - SemaphoreHandle_t m_; + T *buffer_[SIZE]; + std::atomic head_; + std::atomic tail_; + std::atomic dropped_count_; }; } // namespace esp32_ble From be58cdda3ba00a8b0943271d0935294e209ba1c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Jun 2025 16:19:04 -0500 Subject: [PATCH 029/293] Fix protobuf encoding size mismatch by passing force parameter in encode_string (#9074) --- esphome/components/api/proto.h | 2 +- .../host_mode_empty_string_options.yaml | 58 +++++++++ .../test_host_mode_empty_string_options.py | 110 ++++++++++++++++++ 3 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 tests/integration/fixtures/host_mode_empty_string_options.yaml create mode 100644 tests/integration/test_host_mode_empty_string_options.py diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 5265c4520d..eb0dbc151b 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -216,7 +216,7 @@ class ProtoWriteBuffer { this->buffer_->insert(this->buffer_->end(), data, data + len); } void encode_string(uint32_t field_id, const std::string &value, bool force = false) { - this->encode_string(field_id, value.data(), value.size()); + this->encode_string(field_id, value.data(), value.size(), force); } void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) { this->encode_string(field_id, reinterpret_cast(data), len, force); diff --git a/tests/integration/fixtures/host_mode_empty_string_options.yaml b/tests/integration/fixtures/host_mode_empty_string_options.yaml new file mode 100644 index 0000000000..ab8e6cd005 --- /dev/null +++ b/tests/integration/fixtures/host_mode_empty_string_options.yaml @@ -0,0 +1,58 @@ +esphome: + name: host-empty-string-test + +host: + +api: + batch_delay: 50ms + +select: + - platform: template + name: "Select Empty First" + id: select_empty_first + optimistic: true + options: + - "" # Empty string at the beginning + - "Option A" + - "Option B" + - "Option C" + initial_option: "Option A" + + - platform: template + name: "Select Empty Middle" + id: select_empty_middle + optimistic: true + options: + - "Option 1" + - "Option 2" + - "" # Empty string in the middle + - "Option 3" + - "Option 4" + initial_option: "Option 1" + + - platform: template + name: "Select Empty Last" + id: select_empty_last + optimistic: true + options: + - "Choice X" + - "Choice Y" + - "Choice Z" + - "" # Empty string at the end + initial_option: "Choice X" + +# Add a sensor to ensure we have other entities in the list +sensor: + - platform: template + name: "Test Sensor" + id: test_sensor + lambda: |- + return 42.0; + update_interval: 60s + +binary_sensor: + - platform: template + name: "Test Binary Sensor" + id: test_binary_sensor + lambda: |- + return true; diff --git a/tests/integration/test_host_mode_empty_string_options.py b/tests/integration/test_host_mode_empty_string_options.py new file mode 100644 index 0000000000..d2df839a75 --- /dev/null +++ b/tests/integration/test_host_mode_empty_string_options.py @@ -0,0 +1,110 @@ +"""Integration test for protobuf encoding of empty string options in select entities.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityState, SelectInfo +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_host_mode_empty_string_options( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that select entities with empty string options are correctly encoded in protobuf messages. + + This tests the fix for the bug where the force parameter was not passed in encode_string, + causing empty strings in repeated fields to be skipped during encoding but included in + size calculation, leading to protobuf decoding errors. + """ + # Write, compile and run the ESPHome device, then connect to API + loop = asyncio.get_running_loop() + async with run_compiled(yaml_config), api_client_connected() as client: + # Verify we can get device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "host-empty-string-test" + + # Get list of entities - this will encode ListEntitiesSelectResponse messages + # with empty string options that would trigger the bug + entity_info, services = await client.list_entities_services() + + # Find our select entities + select_entities = [e for e in entity_info if isinstance(e, SelectInfo)] + assert len(select_entities) == 3, ( + f"Expected 3 select entities, got {len(select_entities)}" + ) + + # Verify each select entity by name and check their options + selects_by_name = {e.name: e for e in select_entities} + + # Check "Select Empty First" - empty string at beginning + assert "Select Empty First" in selects_by_name + empty_first = selects_by_name["Select Empty First"] + assert len(empty_first.options) == 4 + assert empty_first.options[0] == "" # Empty string at beginning + assert empty_first.options[1] == "Option A" + assert empty_first.options[2] == "Option B" + assert empty_first.options[3] == "Option C" + + # Check "Select Empty Middle" - empty string in middle + assert "Select Empty Middle" in selects_by_name + empty_middle = selects_by_name["Select Empty Middle"] + assert len(empty_middle.options) == 5 + assert empty_middle.options[0] == "Option 1" + assert empty_middle.options[1] == "Option 2" + assert empty_middle.options[2] == "" # Empty string in middle + assert empty_middle.options[3] == "Option 3" + assert empty_middle.options[4] == "Option 4" + + # Check "Select Empty Last" - empty string at end + assert "Select Empty Last" in selects_by_name + empty_last = selects_by_name["Select Empty Last"] + assert len(empty_last.options) == 4 + assert empty_last.options[0] == "Choice X" + assert empty_last.options[1] == "Choice Y" + assert empty_last.options[2] == "Choice Z" + assert empty_last.options[3] == "" # Empty string at end + + # If we got here without protobuf decoding errors, the fix is working + # The bug would have caused "Invalid protobuf message" errors with trailing bytes + + # Also verify we can interact with the select entities + # Subscribe to state changes + states: dict[int, EntityState] = {} + state_change_future: asyncio.Future[None] = loop.create_future() + + def on_state(state: EntityState) -> None: + """Track state changes.""" + states[state.key] = state + # When we receive the state change for our select, resolve the future + if state.key == empty_first.key and not state_change_future.done(): + state_change_future.set_result(None) + + client.subscribe_states(on_state) + + # Try setting a select to an empty string option + # This further tests that empty strings are handled correctly + client.select_command(empty_first.key, "") + + # Wait for state update with timeout + try: + await asyncio.wait_for(state_change_future, timeout=5.0) + except asyncio.TimeoutError: + pytest.fail( + "Did not receive state update after setting select to empty string" + ) + + # Verify the state was set to empty string + assert empty_first.key in states + select_state = states[empty_first.key] + assert hasattr(select_state, "state") + assert select_state.state == "" + + # The test passes if no protobuf decoding errors occurred + # With the bug, we would have gotten "Invalid protobuf message" errors From bd85ba9b6a07d27ced7ed8c52215c2092d05f698 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Sun, 15 Jun 2025 22:19:50 +0100 Subject: [PATCH 030/293] [i2s_audio] Check for a nullptr before disabling and deleting channel (#9062) --- .../microphone/i2s_audio_microphone.cpp | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 1ce98d51d3..52d0ae34fb 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -317,15 +317,18 @@ void I2SAudioMicrophone::stop_driver_() { ESP_LOGW(TAG, "Error uninstalling I2S driver - it may not have started: %s", esp_err_to_name(err)); } #else - /* Have to stop the channel before deleting it */ - err = i2s_channel_disable(this->rx_handle_); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Error stopping I2S microphone - it may not have started: %s", esp_err_to_name(err)); - } - /* If the handle is not needed any more, delete it to release the channel resources */ - err = i2s_del_channel(this->rx_handle_); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Error deleting I2S channel - it may not have started: %s", esp_err_to_name(err)); + if (this->rx_handle_ != nullptr) { + /* Have to stop the channel before deleting it */ + err = i2s_channel_disable(this->rx_handle_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error stopping I2S microphone - it may not have started: %s", esp_err_to_name(err)); + } + /* If the handle is not needed any more, delete it to release the channel resources */ + err = i2s_del_channel(this->rx_handle_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error deleting I2S channel - it may not have started: %s", esp_err_to_name(err)); + } + this->rx_handle_ = nullptr; } #endif this->parent_->unlock(); From 06810e8e6a960221b484972e733ddb845c878222 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Jun 2025 16:22:14 -0500 Subject: [PATCH 031/293] Ensure we can send batches where the first message exceeds MAX_PACKET_SIZE (#9068) --- esphome/components/api/api_connection.cpp | 8 +- tests/integration/conftest.py | 16 +- .../fixtures/api_message_size_batching.yaml | 161 +++++++++++++++ .../fixtures/large_message_batching.yaml | 137 +++++++++++++ .../test_api_message_size_batching.py | 194 ++++++++++++++++++ .../test_large_message_batching.py | 59 ++++++ 6 files changed, 570 insertions(+), 5 deletions(-) create mode 100644 tests/integration/fixtures/api_message_size_batching.yaml create mode 100644 tests/integration/fixtures/large_message_batching.yaml create mode 100644 tests/integration/test_api_message_size_batching.py create mode 100644 tests/integration/test_large_message_batching.py diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ca6e2a2d56..8328f5d2cd 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1807,7 +1807,7 @@ void APIConnection::process_batch_() { this->batch_first_message_ = true; size_t items_processed = 0; - uint32_t remaining_size = MAX_PACKET_SIZE; + uint16_t remaining_size = std::numeric_limits::max(); // Track where each message's header padding begins in the buffer // For plaintext: this is where the 6-byte header padding starts @@ -1832,11 +1832,15 @@ void APIConnection::process_batch_() { packet_info.emplace_back(item.message_type, current_offset, proto_payload_size); // Update tracking variables + items_processed++; + // After first message, set remaining size to MAX_PACKET_SIZE to avoid fragmentation + if (items_processed == 1) { + remaining_size = MAX_PACKET_SIZE; + } remaining_size -= payload_size; // Calculate where the next message's header padding will start // Current buffer size + footer space (that prepare_message_buffer will add for this message) current_offset = this->parent_->get_shared_buffer_ref().size() + footer_size; - items_processed++; } if (items_processed == 0) { diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 4eb1584c27..90377300a6 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -15,7 +15,7 @@ import sys import tempfile from typing import TextIO -from aioesphomeapi import APIClient, APIConnectionError, ReconnectLogic +from aioesphomeapi import APIClient, APIConnectionError, LogParser, ReconnectLogic import pytest import pytest_asyncio @@ -365,11 +365,21 @@ async def _read_stream_lines( stream: asyncio.StreamReader, lines: list[str], output_stream: TextIO ) -> None: """Read lines from a stream, append to list, and echo to output stream.""" + log_parser = LogParser() while line := await stream.readline(): - decoded_line = line.decode("utf-8", errors="replace") + decoded_line = ( + line.replace(b"\r", b"") + .replace(b"\n", b"") + .decode("utf8", "backslashreplace") + ) lines.append(decoded_line.rstrip()) # Echo to stdout/stderr in real-time - print(decoded_line.rstrip(), file=output_stream, flush=True) + # Print without newline to avoid double newlines + print( + log_parser.parse_line(decoded_line, timestamp=""), + file=output_stream, + flush=True, + ) @asynccontextmanager diff --git a/tests/integration/fixtures/api_message_size_batching.yaml b/tests/integration/fixtures/api_message_size_batching.yaml new file mode 100644 index 0000000000..c730dc1aa3 --- /dev/null +++ b/tests/integration/fixtures/api_message_size_batching.yaml @@ -0,0 +1,161 @@ +esphome: + name: message-size-batching-test +host: +api: +# Default batch_delay to test batching +logger: + +# Create entities that will produce different protobuf header sizes +# Header size depends on: 1 byte indicator + varint(payload_size) + varint(message_type) +# 4-byte header: type < 128, payload < 128 +# 5-byte header: type < 128, payload 128-16383 OR type 128+, payload < 128 +# 6-byte header: type 128+, payload 128-16383 + +# Small select with few options - produces small message +select: + - platform: template + name: "Small Select" + id: small_select + optimistic: true + options: + - "Option A" + - "Option B" + initial_option: "Option A" + update_interval: 5.0s + + # Medium select with more options - produces medium message + - platform: template + name: "Medium Select" + id: medium_select + optimistic: true + options: + - "Option 001" + - "Option 002" + - "Option 003" + - "Option 004" + - "Option 005" + - "Option 006" + - "Option 007" + - "Option 008" + - "Option 009" + - "Option 010" + - "Option 011" + - "Option 012" + - "Option 013" + - "Option 014" + - "Option 015" + - "Option 016" + - "Option 017" + - "Option 018" + - "Option 019" + - "Option 020" + initial_option: "Option 001" + update_interval: 5.0s + + # Large select with many options - produces larger message + - platform: template + name: "Large Select with Many Options to Create Larger Payload" + id: large_select + optimistic: true + options: + - "Long Option Name 001 - This is a longer option name to increase message size" + - "Long Option Name 002 - This is a longer option name to increase message size" + - "Long Option Name 003 - This is a longer option name to increase message size" + - "Long Option Name 004 - This is a longer option name to increase message size" + - "Long Option Name 005 - This is a longer option name to increase message size" + - "Long Option Name 006 - This is a longer option name to increase message size" + - "Long Option Name 007 - This is a longer option name to increase message size" + - "Long Option Name 008 - This is a longer option name to increase message size" + - "Long Option Name 009 - This is a longer option name to increase message size" + - "Long Option Name 010 - This is a longer option name to increase message size" + - "Long Option Name 011 - This is a longer option name to increase message size" + - "Long Option Name 012 - This is a longer option name to increase message size" + - "Long Option Name 013 - This is a longer option name to increase message size" + - "Long Option Name 014 - This is a longer option name to increase message size" + - "Long Option Name 015 - This is a longer option name to increase message size" + - "Long Option Name 016 - This is a longer option name to increase message size" + - "Long Option Name 017 - This is a longer option name to increase message size" + - "Long Option Name 018 - This is a longer option name to increase message size" + - "Long Option Name 019 - This is a longer option name to increase message size" + - "Long Option Name 020 - This is a longer option name to increase message size" + - "Long Option Name 021 - This is a longer option name to increase message size" + - "Long Option Name 022 - This is a longer option name to increase message size" + - "Long Option Name 023 - This is a longer option name to increase message size" + - "Long Option Name 024 - This is a longer option name to increase message size" + - "Long Option Name 025 - This is a longer option name to increase message size" + - "Long Option Name 026 - This is a longer option name to increase message size" + - "Long Option Name 027 - This is a longer option name to increase message size" + - "Long Option Name 028 - This is a longer option name to increase message size" + - "Long Option Name 029 - This is a longer option name to increase message size" + - "Long Option Name 030 - This is a longer option name to increase message size" + - "Long Option Name 031 - This is a longer option name to increase message size" + - "Long Option Name 032 - This is a longer option name to increase message size" + - "Long Option Name 033 - This is a longer option name to increase message size" + - "Long Option Name 034 - This is a longer option name to increase message size" + - "Long Option Name 035 - This is a longer option name to increase message size" + - "Long Option Name 036 - This is a longer option name to increase message size" + - "Long Option Name 037 - This is a longer option name to increase message size" + - "Long Option Name 038 - This is a longer option name to increase message size" + - "Long Option Name 039 - This is a longer option name to increase message size" + - "Long Option Name 040 - This is a longer option name to increase message size" + - "Long Option Name 041 - This is a longer option name to increase message size" + - "Long Option Name 042 - This is a longer option name to increase message size" + - "Long Option Name 043 - This is a longer option name to increase message size" + - "Long Option Name 044 - This is a longer option name to increase message size" + - "Long Option Name 045 - This is a longer option name to increase message size" + - "Long Option Name 046 - This is a longer option name to increase message size" + - "Long Option Name 047 - This is a longer option name to increase message size" + - "Long Option Name 048 - This is a longer option name to increase message size" + - "Long Option Name 049 - This is a longer option name to increase message size" + - "Long Option Name 050 - This is a longer option name to increase message size" + initial_option: "Long Option Name 001 - This is a longer option name to increase message size" + update_interval: 5.0s + +# Text sensors with different value lengths +text_sensor: + - platform: template + name: "Short Text Sensor" + id: short_text_sensor + lambda: |- + return {"OK"}; + update_interval: 5.0s + + - platform: template + name: "Medium Text Sensor" + id: medium_text_sensor + lambda: |- + return {"This is a medium length text sensor value that should produce a medium sized message"}; + update_interval: 5.0s + + - platform: template + name: "Long Text Sensor with Very Long Value" + id: long_text_sensor + lambda: |- + return {"This is a very long text sensor value that contains a lot of text to ensure we get a larger protobuf message. The message should be long enough to require a 2-byte varint for the payload size, which happens when the payload exceeds 127 bytes. Let's add even more text here to make sure we exceed that threshold and test the batching of messages with different header sizes properly."}; + update_interval: 5.0s + +# Text input which can have various lengths +text: + - platform: template + name: "Test Text Input" + id: test_text_input + optimistic: true + mode: text + min_length: 0 + max_length: 255 + initial_value: "Initial value" + update_interval: 5.0s + +# Number entity to add variety (different message type number) +# The ListEntitiesNumberResponse has message type 49 +# The NumberStateResponse has message type 50 +number: + - platform: template + name: "Test Number with Long Name to Increase Message Size" + id: test_number + optimistic: true + min_value: 0 + max_value: 1000 + step: 0.1 + initial_value: 42.0 + update_interval: 5.0s diff --git a/tests/integration/fixtures/large_message_batching.yaml b/tests/integration/fixtures/large_message_batching.yaml new file mode 100644 index 0000000000..1b2d817cd4 --- /dev/null +++ b/tests/integration/fixtures/large_message_batching.yaml @@ -0,0 +1,137 @@ +esphome: + name: large-message-test +host: +api: +logger: + +# Create a select entity with many options to exceed 1390 bytes +select: + - platform: template + name: "Large Select" + id: large_select + optimistic: true + options: + - "Option 000 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 001 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 002 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 003 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 004 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 005 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 006 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 007 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 008 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 009 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 010 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 011 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 012 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 013 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 014 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 015 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 016 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 017 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 018 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 019 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 020 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 021 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 022 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 023 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 024 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 025 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 026 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 027 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 028 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 029 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 030 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 031 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 032 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 033 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 034 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 035 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 036 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 037 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 038 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 039 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 040 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 041 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 042 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 043 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 044 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 045 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 046 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 047 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 048 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 049 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 050 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 051 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 052 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 053 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 054 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 055 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 056 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 057 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 058 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 059 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 060 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 061 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 062 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 063 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 064 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 065 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 066 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 067 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 068 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 069 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 070 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 071 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 072 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 073 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 074 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 075 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 076 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 077 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 078 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 079 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 080 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 081 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 082 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 083 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 084 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 085 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 086 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 087 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 088 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 089 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 090 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 091 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 092 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 093 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 094 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 095 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 096 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 097 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 098 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + - "Option 099 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + initial_option: "Option 000 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + +# Add some other entities to test batching with the large select +sensor: + - platform: template + name: "Test Sensor" + id: test_sensor + lambda: |- + return 42.0; + update_interval: 1s + +binary_sensor: + - platform: template + name: "Test Binary Sensor" + id: test_binary_sensor + lambda: |- + return true; + +switch: + - platform: template + name: "Test Switch" + id: test_switch + optimistic: true + diff --git a/tests/integration/test_api_message_size_batching.py b/tests/integration/test_api_message_size_batching.py new file mode 100644 index 0000000000..631e64825e --- /dev/null +++ b/tests/integration/test_api_message_size_batching.py @@ -0,0 +1,194 @@ +"""Integration test for API batching with various message sizes.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityState, NumberInfo, SelectInfo, TextInfo, TextSensorInfo +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_api_message_size_batching( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test API can batch messages of various sizes correctly.""" + # Write, compile and run the ESPHome device, then connect to API + loop = asyncio.get_running_loop() + async with run_compiled(yaml_config), api_client_connected() as client: + # Verify we can get device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "message-size-batching-test" + + # List entities - this will batch various sized messages together + entity_info, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Count different entity types + selects = [] + text_sensors = [] + text_inputs = [] + numbers = [] + other_entities = [] + + for entity in entity_info: + if isinstance(entity, SelectInfo): + selects.append(entity) + elif isinstance(entity, TextSensorInfo): + text_sensors.append(entity) + elif isinstance(entity, TextInfo): + text_inputs.append(entity) + elif isinstance(entity, NumberInfo): + numbers.append(entity) + else: + other_entities.append(entity) + + # Verify we have our test entities - exact counts + assert len(selects) == 3, ( + f"Expected exactly 3 select entities, got {len(selects)}" + ) + assert len(text_sensors) == 3, ( + f"Expected exactly 3 text sensor entities, got {len(text_sensors)}" + ) + assert len(text_inputs) == 1, ( + f"Expected exactly 1 text input entity, got {len(text_inputs)}" + ) + + # Collect all select entity object_ids for error messages + select_ids = [s.object_id for s in selects] + + # Find our specific test entities + small_select = None + medium_select = None + large_select = None + + for select in selects: + if select.object_id == "small_select": + small_select = select + elif select.object_id == "medium_select": + medium_select = select + elif ( + select.object_id + == "large_select_with_many_options_to_create_larger_payload" + ): + large_select = select + + assert small_select is not None, ( + f"Could not find small_select entity. Found: {select_ids}" + ) + assert medium_select is not None, ( + f"Could not find medium_select entity. Found: {select_ids}" + ) + assert large_select is not None, ( + f"Could not find large_select entity. Found: {select_ids}" + ) + + # Verify the selects have the expected number of options + assert len(small_select.options) == 2, ( + f"Expected 2 options for small_select, got {len(small_select.options)}" + ) + assert len(medium_select.options) == 20, ( + f"Expected 20 options for medium_select, got {len(medium_select.options)}" + ) + assert len(large_select.options) == 50, ( + f"Expected 50 options for large_select, got {len(large_select.options)}" + ) + + # Collect all text sensor object_ids for error messages + text_sensor_ids = [t.object_id for t in text_sensors] + + # Verify text sensors with different value lengths + short_text_sensor = None + medium_text_sensor = None + long_text_sensor = None + + for text_sensor in text_sensors: + if text_sensor.object_id == "short_text_sensor": + short_text_sensor = text_sensor + elif text_sensor.object_id == "medium_text_sensor": + medium_text_sensor = text_sensor + elif text_sensor.object_id == "long_text_sensor_with_very_long_value": + long_text_sensor = text_sensor + + assert short_text_sensor is not None, ( + f"Could not find short_text_sensor. Found: {text_sensor_ids}" + ) + assert medium_text_sensor is not None, ( + f"Could not find medium_text_sensor. Found: {text_sensor_ids}" + ) + assert long_text_sensor is not None, ( + f"Could not find long_text_sensor. Found: {text_sensor_ids}" + ) + + # Check text input which can have a long max_length + text_input = None + text_input_ids = [t.object_id for t in text_inputs] + + for ti in text_inputs: + if ti.object_id == "test_text_input": + text_input = ti + break + + assert text_input is not None, ( + f"Could not find test_text_input. Found: {text_input_ids}" + ) + assert text_input.max_length == 255, ( + f"Expected max_length 255, got {text_input.max_length}" + ) + + # Verify total entity count - messages of various sizes were batched successfully + # We have: 3 selects + 3 text sensors + 1 text input + 1 number = 8 total + total_entities = len(entity_info) + assert total_entities == 8, f"Expected exactly 8 entities, got {total_entities}" + + # Check we have the expected entity types + assert len(numbers) == 1, ( + f"Expected exactly 1 number entity, got {len(numbers)}" + ) + assert len(other_entities) == 0, ( + f"Unexpected entity types found: {[type(e).__name__ for e in other_entities]}" + ) + + # Subscribe to state changes to verify batching works + # Collect keys from entity info to know what states to expect + expected_keys = {entity.key for entity in entity_info} + assert len(expected_keys) == 8, ( + f"Expected 8 unique entity keys, got {len(expected_keys)}" + ) + + received_keys: set[int] = set() + states_future: asyncio.Future[None] = loop.create_future() + + def on_state(state: EntityState) -> None: + """Track when states are received.""" + received_keys.add(state.key) + # Check if we've received states from all expected entities + if expected_keys.issubset(received_keys) and not states_future.done(): + states_future.set_result(None) + + client.subscribe_states(on_state) + + # Wait for states with timeout + try: + await asyncio.wait_for(states_future, timeout=5.0) + except asyncio.TimeoutError: + missing_keys = expected_keys - received_keys + pytest.fail( + f"Did not receive states from all entities within 5 seconds. " + f"Missing keys: {missing_keys}, " + f"Received {len(received_keys)} of {len(expected_keys)} expected states" + ) + + # Verify we received states from all entities + assert expected_keys.issubset(received_keys) + + # Check that various message sizes were handled correctly + # Small messages (4-byte header): type < 128, payload < 128 + # Medium messages (5-byte header): type < 128, payload 128-16383 OR type 128+, payload < 128 + # Large messages (6-byte header): type 128+, payload 128-16383 diff --git a/tests/integration/test_large_message_batching.py b/tests/integration/test_large_message_batching.py new file mode 100644 index 0000000000..399fd39dd3 --- /dev/null +++ b/tests/integration/test_large_message_batching.py @@ -0,0 +1,59 @@ +"""Integration test for API handling of large messages exceeding batch size.""" + +from __future__ import annotations + +from aioesphomeapi import SelectInfo +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_large_message_batching( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test API can handle large messages (>1390 bytes) in batches.""" + # Write, compile and run the ESPHome device, then connect to API + async with run_compiled(yaml_config), api_client_connected() as client: + # Verify we can get device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "large-message-test" + + # List entities - this will include our select with many options + entity_info, services = await client.list_entities_services() + + # Find our large select entity + large_select = None + for entity in entity_info: + if isinstance(entity, SelectInfo) and entity.object_id == "large_select": + large_select = entity + break + + assert large_select is not None, "Could not find large_select entity" + + # Verify the select has all its options + # We created 100 options with long names + assert len(large_select.options) == 100, ( + f"Expected 100 options, got {len(large_select.options)}" + ) + + # Verify all options are present and correct + for i in range(100): + expected_option = f"Option {i:03d} - This is a very long option name to make the message larger than the typical batch size of 1390 bytes" + assert expected_option in large_select.options, ( + f"Missing option: {expected_option}" + ) + + # Also verify we can still receive other entities in the same batch + # Count total entities - should have at least our select plus some sensors + entity_count = len(entity_info) + assert entity_count >= 4, f"Expected at least 4 entities, got {entity_count}" + + # Verify we have different entity types (not just selects) + entity_types = {type(entity).__name__ for entity in entity_info} + assert len(entity_types) >= 2, ( + f"Expected multiple entity types, got {entity_types}" + ) From 1dbebe90bac2783618365948c9159933b5ee1e19 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Jun 2025 16:29:25 -0500 Subject: [PATCH 032/293] Add common base classes for entity protobuf messages to reduce duplicate code (#9090) --- esphome/components/api/api.proto | 44 ++++ esphome/components/api/api_connection.cpp | 44 ++-- esphome/components/api/api_connection.h | 9 +- esphome/components/api/api_options.proto | 1 + esphome/components/api/api_pb2.cpp | 1 + esphome/components/api/api_pb2.h | 291 +++++----------------- script/api_protobuf/api_protobuf.py | 171 ++++++++++++- 7 files changed, 306 insertions(+), 255 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index c5c63b8dfc..843b72795a 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -266,6 +266,7 @@ enum EntityCategory { // ==================== BINARY SENSOR ==================== message ListEntitiesBinarySensorResponse { option (id) = 12; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_BINARY_SENSOR"; @@ -282,6 +283,7 @@ message ListEntitiesBinarySensorResponse { } message BinarySensorStateResponse { option (id) = 21; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_BINARY_SENSOR"; option (no_delay) = true; @@ -296,6 +298,7 @@ message BinarySensorStateResponse { // ==================== COVER ==================== message ListEntitiesCoverResponse { option (id) = 13; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_COVER"; @@ -325,6 +328,7 @@ enum CoverOperation { } message CoverStateResponse { option (id) = 22; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_COVER"; option (no_delay) = true; @@ -367,6 +371,7 @@ message CoverCommandRequest { // ==================== FAN ==================== message ListEntitiesFanResponse { option (id) = 14; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_FAN"; @@ -395,6 +400,7 @@ enum FanDirection { } message FanStateResponse { option (id) = 23; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_FAN"; option (no_delay) = true; @@ -444,6 +450,7 @@ enum ColorMode { } message ListEntitiesLightResponse { option (id) = 15; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_LIGHT"; @@ -467,6 +474,7 @@ message ListEntitiesLightResponse { } message LightStateResponse { option (id) = 24; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_LIGHT"; option (no_delay) = true; @@ -536,6 +544,7 @@ enum SensorLastResetType { message ListEntitiesSensorResponse { option (id) = 16; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_SENSOR"; @@ -557,6 +566,7 @@ message ListEntitiesSensorResponse { } message SensorStateResponse { option (id) = 25; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_SENSOR"; option (no_delay) = true; @@ -571,6 +581,7 @@ message SensorStateResponse { // ==================== SWITCH ==================== message ListEntitiesSwitchResponse { option (id) = 17; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_SWITCH"; @@ -587,6 +598,7 @@ message ListEntitiesSwitchResponse { } message SwitchStateResponse { option (id) = 26; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_SWITCH"; option (no_delay) = true; @@ -607,6 +619,7 @@ message SwitchCommandRequest { // ==================== TEXT SENSOR ==================== message ListEntitiesTextSensorResponse { option (id) = 18; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_TEXT_SENSOR"; @@ -622,6 +635,7 @@ message ListEntitiesTextSensorResponse { } message TextSensorStateResponse { option (id) = 27; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_TEXT_SENSOR"; option (no_delay) = true; @@ -789,6 +803,7 @@ message ExecuteServiceRequest { // ==================== CAMERA ==================== message ListEntitiesCameraResponse { option (id) = 43; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_ESP32_CAMERA"; @@ -869,6 +884,7 @@ enum ClimatePreset { } message ListEntitiesClimateResponse { option (id) = 46; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_CLIMATE"; @@ -903,6 +919,7 @@ message ListEntitiesClimateResponse { } message ClimateStateResponse { option (id) = 47; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_CLIMATE"; option (no_delay) = true; @@ -964,6 +981,7 @@ enum NumberMode { } message ListEntitiesNumberResponse { option (id) = 49; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_NUMBER"; @@ -984,6 +1002,7 @@ message ListEntitiesNumberResponse { } message NumberStateResponse { option (id) = 50; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_NUMBER"; option (no_delay) = true; @@ -1007,6 +1026,7 @@ message NumberCommandRequest { // ==================== SELECT ==================== message ListEntitiesSelectResponse { option (id) = 52; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_SELECT"; @@ -1022,6 +1042,7 @@ message ListEntitiesSelectResponse { } message SelectStateResponse { option (id) = 53; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_SELECT"; option (no_delay) = true; @@ -1045,6 +1066,7 @@ message SelectCommandRequest { // ==================== SIREN ==================== message ListEntitiesSirenResponse { option (id) = 55; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_SIREN"; @@ -1062,6 +1084,7 @@ message ListEntitiesSirenResponse { } message SirenStateResponse { option (id) = 56; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_SIREN"; option (no_delay) = true; @@ -1102,6 +1125,7 @@ enum LockCommand { } message ListEntitiesLockResponse { option (id) = 58; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_LOCK"; @@ -1123,6 +1147,7 @@ message ListEntitiesLockResponse { } message LockStateResponse { option (id) = 59; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_LOCK"; option (no_delay) = true; @@ -1145,6 +1170,7 @@ message LockCommandRequest { // ==================== BUTTON ==================== message ListEntitiesButtonResponse { option (id) = 61; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_BUTTON"; @@ -1196,6 +1222,7 @@ message MediaPlayerSupportedFormat { } message ListEntitiesMediaPlayerResponse { option (id) = 63; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_MEDIA_PLAYER"; @@ -1214,6 +1241,7 @@ message ListEntitiesMediaPlayerResponse { } message MediaPlayerStateResponse { option (id) = 64; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_MEDIA_PLAYER"; option (no_delay) = true; @@ -1735,6 +1763,7 @@ enum AlarmControlPanelStateCommand { message ListEntitiesAlarmControlPanelResponse { option (id) = 94; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_ALARM_CONTROL_PANEL"; @@ -1752,6 +1781,7 @@ message ListEntitiesAlarmControlPanelResponse { message AlarmControlPanelStateResponse { option (id) = 95; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_ALARM_CONTROL_PANEL"; option (no_delay) = true; @@ -1776,6 +1806,7 @@ enum TextMode { } message ListEntitiesTextResponse { option (id) = 97; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_TEXT"; @@ -1794,6 +1825,7 @@ message ListEntitiesTextResponse { } message TextStateResponse { option (id) = 98; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_TEXT"; option (no_delay) = true; @@ -1818,6 +1850,7 @@ message TextCommandRequest { // ==================== DATETIME DATE ==================== message ListEntitiesDateResponse { option (id) = 100; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_DATE"; @@ -1832,6 +1865,7 @@ message ListEntitiesDateResponse { } message DateStateResponse { option (id) = 101; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_DATE"; option (no_delay) = true; @@ -1859,6 +1893,7 @@ message DateCommandRequest { // ==================== DATETIME TIME ==================== message ListEntitiesTimeResponse { option (id) = 103; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_TIME"; @@ -1873,6 +1908,7 @@ message ListEntitiesTimeResponse { } message TimeStateResponse { option (id) = 104; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_TIME"; option (no_delay) = true; @@ -1900,6 +1936,7 @@ message TimeCommandRequest { // ==================== EVENT ==================== message ListEntitiesEventResponse { option (id) = 107; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_EVENT"; @@ -1917,6 +1954,7 @@ message ListEntitiesEventResponse { } message EventResponse { option (id) = 108; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_EVENT"; @@ -1927,6 +1965,7 @@ message EventResponse { // ==================== VALVE ==================== message ListEntitiesValveResponse { option (id) = 109; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_VALVE"; @@ -1952,6 +1991,7 @@ enum ValveOperation { } message ValveStateResponse { option (id) = 110; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_VALVE"; option (no_delay) = true; @@ -1976,6 +2016,7 @@ message ValveCommandRequest { // ==================== DATETIME DATETIME ==================== message ListEntitiesDateTimeResponse { option (id) = 112; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_DATETIME"; @@ -1990,6 +2031,7 @@ message ListEntitiesDateTimeResponse { } message DateTimeStateResponse { option (id) = 113; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_DATETIME"; option (no_delay) = true; @@ -2013,6 +2055,7 @@ message DateTimeCommandRequest { // ==================== UPDATE ==================== message ListEntitiesUpdateResponse { option (id) = 116; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_UPDATE"; @@ -2028,6 +2071,7 @@ message ListEntitiesUpdateResponse { } message UpdateStateResponse { option (id) = 117; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_UPDATE"; option (no_delay) = true; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8328f5d2cd..3e2b7c0154 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -301,7 +301,7 @@ uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConn BinarySensorStateResponse resp; resp.state = binary_sensor->state; resp.missing_state = !binary_sensor->has_state(); - resp.key = binary_sensor->get_object_id_hash(); + fill_entity_state_base(binary_sensor, resp); return encode_message_to_buffer(resp, BinarySensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -335,7 +335,7 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection * if (traits.get_supports_tilt()) msg.tilt = cover->tilt; msg.current_operation = static_cast(cover->current_operation); - msg.key = cover->get_object_id_hash(); + fill_entity_state_base(cover, msg); return encode_message_to_buffer(msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -403,7 +403,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co msg.direction = static_cast(fan->direction); if (traits.supports_preset_modes()) msg.preset_mode = fan->preset_mode; - msg.key = fan->get_object_id_hash(); + fill_entity_state_base(fan, msg); return encode_message_to_buffer(msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -470,7 +470,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * resp.warm_white = values.get_warm_white(); if (light->supports_effects()) resp.effect = light->get_effect_name(); - resp.key = light->get_object_id_hash(); + fill_entity_state_base(light, resp); return encode_message_to_buffer(resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -552,7 +552,7 @@ uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection SensorStateResponse resp; resp.state = sensor->state; resp.missing_state = !sensor->has_state(); - resp.key = sensor->get_object_id_hash(); + fill_entity_state_base(sensor, resp); return encode_message_to_buffer(resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -586,7 +586,7 @@ uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection auto *a_switch = static_cast(entity); SwitchStateResponse resp; resp.state = a_switch->state; - resp.key = a_switch->get_object_id_hash(); + fill_entity_state_base(a_switch, resp); return encode_message_to_buffer(resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -629,7 +629,7 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec TextSensorStateResponse resp; resp.state = text_sensor->state; resp.missing_state = !text_sensor->has_state(); - resp.key = text_sensor->get_object_id_hash(); + fill_entity_state_base(text_sensor, resp); return encode_message_to_buffer(resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -653,7 +653,7 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection bool is_single) { auto *climate = static_cast(entity); ClimateStateResponse resp; - resp.key = climate->get_object_id_hash(); + fill_entity_state_base(climate, resp); auto traits = climate->get_traits(); resp.mode = static_cast(climate->mode); resp.action = static_cast(climate->action); @@ -762,7 +762,7 @@ uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection NumberStateResponse resp; resp.state = number->state; resp.missing_state = !number->has_state(); - resp.key = number->get_object_id_hash(); + fill_entity_state_base(number, resp); return encode_message_to_buffer(resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -803,7 +803,7 @@ uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *c resp.year = date->year; resp.month = date->month; resp.day = date->day; - resp.key = date->get_object_id_hash(); + fill_entity_state_base(date, resp); return encode_message_to_buffer(resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::send_date_info(datetime::DateEntity *date) { @@ -840,7 +840,7 @@ uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *c resp.hour = time->hour; resp.minute = time->minute; resp.second = time->second; - resp.key = time->get_object_id_hash(); + fill_entity_state_base(time, resp); return encode_message_to_buffer(resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::send_time_info(datetime::TimeEntity *time) { @@ -879,7 +879,7 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio ESPTime state = datetime->state_as_esptime(); resp.epoch_seconds = state.timestamp; } - resp.key = datetime->get_object_id_hash(); + fill_entity_state_base(datetime, resp); return encode_message_to_buffer(resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) { @@ -918,7 +918,7 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c TextStateResponse resp; resp.state = text->state; resp.missing_state = !text->has_state(); - resp.key = text->get_object_id_hash(); + fill_entity_state_base(text, resp); return encode_message_to_buffer(resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -959,7 +959,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection SelectStateResponse resp; resp.state = select->state; resp.missing_state = !select->has_state(); - resp.key = select->get_object_id_hash(); + fill_entity_state_base(select, resp); return encode_message_to_buffer(resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -1019,7 +1019,7 @@ uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *c auto *a_lock = static_cast(entity); LockStateResponse resp; resp.state = static_cast(a_lock->state); - resp.key = a_lock->get_object_id_hash(); + fill_entity_state_base(a_lock, resp); return encode_message_to_buffer(resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -1063,7 +1063,7 @@ uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection * ValveStateResponse resp; resp.position = valve->position; resp.current_operation = static_cast(valve->current_operation); - resp.key = valve->get_object_id_hash(); + fill_entity_state_base(valve, resp); return encode_message_to_buffer(resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::send_valve_info(valve::Valve *valve) { @@ -1111,7 +1111,7 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne resp.state = static_cast(report_state); resp.volume = media_player->volume; resp.muted = media_player->is_muted(); - resp.key = media_player->get_object_id_hash(); + fill_entity_state_base(media_player, resp); return encode_message_to_buffer(resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) { @@ -1375,7 +1375,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, A auto *a_alarm_control_panel = static_cast(entity); AlarmControlPanelStateResponse resp; resp.state = static_cast(a_alarm_control_panel->get_state()); - resp.key = a_alarm_control_panel->get_object_id_hash(); + fill_entity_state_base(a_alarm_control_panel, resp); return encode_message_to_buffer(resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { @@ -1439,7 +1439,7 @@ uint16_t APIConnection::try_send_event_response(event::Event *event, const std:: uint32_t remaining_size, bool is_single) { EventResponse resp; resp.event_type = event_type; - resp.key = event->get_object_id_hash(); + fill_entity_state_base(event, resp); return encode_message_to_buffer(resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -1477,7 +1477,7 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection resp.release_summary = update->update_info.summary; resp.release_url = update->update_info.release_url; } - resp.key = update->get_object_id_hash(); + fill_entity_state_base(update, resp); return encode_message_to_buffer(resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::send_update_info(update::UpdateEntity *update) { @@ -1538,7 +1538,7 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char buffer.encode_string(3, line, line_length); // string message = 3 // SubscribeLogsResponse - 29 - return this->send_buffer(buffer, 29); + return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE); } HelloResponse APIConnection::hello(const HelloRequest &msg) { @@ -1685,7 +1685,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { return false; } bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) { - if (!this->try_to_clear_buffer(message_type != 29)) { // SubscribeLogsResponse + if (!this->try_to_clear_buffer(message_type != SubscribeLogsResponse::MESSAGE_TYPE)) { // SubscribeLogsResponse return false; } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 13e6066788..7cd41561d4 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -282,8 +282,8 @@ class APIConnection : public APIServerConnection { ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size); protected: - // Helper function to fill common entity fields - template static void fill_entity_info_base(esphome::EntityBase *entity, ResponseT &response) { + // Helper function to fill common entity info fields + static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) { // Set common fields that are shared by all entity types response.key = entity->get_object_id_hash(); response.object_id = entity->get_object_id(); @@ -297,6 +297,11 @@ class APIConnection : public APIServerConnection { response.entity_category = static_cast(entity->get_entity_category()); } + // Helper function to fill common entity state fields + static void fill_entity_state_base(esphome::EntityBase *entity, StateResponseProtoMessage &response) { + response.key = entity->get_object_id_hash(); + } + // Non-template helper to encode any ProtoMessage static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, uint32_t remaining_size, bool is_single); diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index feaf39ba15..3a547b8688 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -21,4 +21,5 @@ extend google.protobuf.MessageOptions { optional string ifdef = 1038; optional bool log = 1039 [default=true]; optional bool no_delay = 1040 [default=false]; + optional string base_class = 1041; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 2d609f6dd6..415409f880 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -628,6 +628,7 @@ template<> const char *proto_enum_to_string(enums::UpdateC } } #endif + bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 8b3f7a7b2a..14a1f3f353 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -253,6 +253,27 @@ enum UpdateCommand : uint32_t { } // namespace enums +class InfoResponseProtoMessage : public ProtoMessage { + public: + ~InfoResponseProtoMessage() override = default; + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + bool disabled_by_default{false}; + std::string icon{}; + enums::EntityCategory entity_category{}; + + protected: +}; + +class StateResponseProtoMessage : public ProtoMessage { + public: + ~StateResponseProtoMessage() override = default; + uint32_t key{0}; + + protected: +}; class HelloRequest : public ProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 1; @@ -484,22 +505,15 @@ class SubscribeStatesRequest : public ProtoMessage { protected: }; -class ListEntitiesBinarySensorResponse : public ProtoMessage { +class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 12; static constexpr uint16_t ESTIMATED_SIZE = 56; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_binary_sensor_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; std::string device_class{}; bool is_status_binary_sensor{false}; - bool disabled_by_default{false}; - std::string icon{}; - enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -511,14 +525,13 @@ class ListEntitiesBinarySensorResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BinarySensorStateResponse : public ProtoMessage { +class BinarySensorStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 21; static constexpr uint16_t ESTIMATED_SIZE = 9; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "binary_sensor_state_response"; } #endif - uint32_t key{0}; bool state{false}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -531,24 +544,17 @@ class BinarySensorStateResponse : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesCoverResponse : public ProtoMessage { +class ListEntitiesCoverResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 13; static constexpr uint16_t ESTIMATED_SIZE = 62; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_cover_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; bool assumed_state{false}; bool supports_position{false}; bool supports_tilt{false}; std::string device_class{}; - bool disabled_by_default{false}; - std::string icon{}; - enums::EntityCategory entity_category{}; bool supports_stop{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -561,14 +567,13 @@ class ListEntitiesCoverResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class CoverStateResponse : public ProtoMessage { +class CoverStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 22; static constexpr uint16_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "cover_state_response"; } #endif - uint32_t key{0}; enums::LegacyCoverState legacy_state{}; float position{0.0f}; float tilt{0.0f}; @@ -608,24 +613,17 @@ class CoverCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesFanResponse : public ProtoMessage { +class ListEntitiesFanResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 14; static constexpr uint16_t ESTIMATED_SIZE = 73; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_fan_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; bool supports_oscillation{false}; bool supports_speed{false}; bool supports_direction{false}; int32_t supported_speed_count{0}; - bool disabled_by_default{false}; - std::string icon{}; - enums::EntityCategory entity_category{}; std::vector supported_preset_modes{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -638,14 +636,13 @@ class ListEntitiesFanResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class FanStateResponse : public ProtoMessage { +class FanStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 23; static constexpr uint16_t ESTIMATED_SIZE = 26; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "fan_state_response"; } #endif - uint32_t key{0}; bool state{false}; bool oscillating{false}; enums::FanSpeed speed{}; @@ -694,17 +691,13 @@ class FanCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesLightResponse : public ProtoMessage { +class ListEntitiesLightResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 15; static constexpr uint16_t ESTIMATED_SIZE = 85; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_light_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; std::vector supported_color_modes{}; bool legacy_supports_brightness{false}; bool legacy_supports_rgb{false}; @@ -713,9 +706,6 @@ class ListEntitiesLightResponse : public ProtoMessage { float min_mireds{0.0f}; float max_mireds{0.0f}; std::vector effects{}; - bool disabled_by_default{false}; - std::string icon{}; - enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -727,14 +717,13 @@ class ListEntitiesLightResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class LightStateResponse : public ProtoMessage { +class LightStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 24; static constexpr uint16_t ESTIMATED_SIZE = 63; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "light_state_response"; } #endif - uint32_t key{0}; bool state{false}; float brightness{0.0f}; enums::ColorMode color_mode{}; @@ -803,26 +792,19 @@ class LightCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesSensorResponse : public ProtoMessage { +class ListEntitiesSensorResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 16; static constexpr uint16_t ESTIMATED_SIZE = 73; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_sensor_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; - std::string icon{}; std::string unit_of_measurement{}; int32_t accuracy_decimals{0}; bool force_update{false}; std::string device_class{}; enums::SensorStateClass state_class{}; enums::SensorLastResetType legacy_last_reset_type{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -834,14 +816,13 @@ class ListEntitiesSensorResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class SensorStateResponse : public ProtoMessage { +class SensorStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 25; static constexpr uint16_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "sensor_state_response"; } #endif - uint32_t key{0}; float state{0.0f}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -854,21 +835,14 @@ class SensorStateResponse : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesSwitchResponse : public ProtoMessage { +class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 17; static constexpr uint16_t ESTIMATED_SIZE = 56; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_switch_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; - std::string icon{}; bool assumed_state{false}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -881,14 +855,13 @@ class ListEntitiesSwitchResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class SwitchStateResponse : public ProtoMessage { +class SwitchStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 26; static constexpr uint16_t ESTIMATED_SIZE = 7; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "switch_state_response"; } #endif - uint32_t key{0}; bool state{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -919,20 +892,13 @@ class SwitchCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesTextSensorResponse : public ProtoMessage { +class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 18; static constexpr uint16_t ESTIMATED_SIZE = 54; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_text_sensor_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -945,14 +911,13 @@ class ListEntitiesTextSensorResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class TextSensorStateResponse : public ProtoMessage { +class TextSensorStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 27; static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "text_sensor_state_response"; } #endif - uint32_t key{0}; std::string state{}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -1249,20 +1214,13 @@ class ExecuteServiceRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class ListEntitiesCameraResponse : public ProtoMessage { +class ListEntitiesCameraResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 43; static constexpr uint16_t ESTIMATED_SIZE = 45; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_camera_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; - bool disabled_by_default{false}; - std::string icon{}; - enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1313,17 +1271,13 @@ class CameraImageRequest : public ProtoMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesClimateResponse : public ProtoMessage { +class ListEntitiesClimateResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 46; static constexpr uint16_t ESTIMATED_SIZE = 151; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_climate_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; bool supports_current_temperature{false}; bool supports_two_point_target_temperature{false}; std::vector supported_modes{}; @@ -1337,9 +1291,6 @@ class ListEntitiesClimateResponse : public ProtoMessage { std::vector supported_custom_fan_modes{}; std::vector supported_presets{}; std::vector supported_custom_presets{}; - bool disabled_by_default{false}; - std::string icon{}; - enums::EntityCategory entity_category{}; float visual_current_temperature_step{0.0f}; bool supports_current_humidity{false}; bool supports_target_humidity{false}; @@ -1356,14 +1307,13 @@ class ListEntitiesClimateResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ClimateStateResponse : public ProtoMessage { +class ClimateStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 47; static constexpr uint16_t ESTIMATED_SIZE = 65; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "climate_state_response"; } #endif - uint32_t key{0}; enums::ClimateMode mode{}; float current_temperature{0.0f}; float target_temperature{0.0f}; @@ -1430,23 +1380,16 @@ class ClimateCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesNumberResponse : public ProtoMessage { +class ListEntitiesNumberResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 49; static constexpr uint16_t ESTIMATED_SIZE = 80; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_number_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; - std::string icon{}; float min_value{0.0f}; float max_value{0.0f}; float step{0.0f}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; std::string unit_of_measurement{}; enums::NumberMode mode{}; std::string device_class{}; @@ -1461,14 +1404,13 @@ class ListEntitiesNumberResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class NumberStateResponse : public ProtoMessage { +class NumberStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 50; static constexpr uint16_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "number_state_response"; } #endif - uint32_t key{0}; float state{0.0f}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -1499,21 +1441,14 @@ class NumberCommandRequest : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; }; -class ListEntitiesSelectResponse : public ProtoMessage { +class ListEntitiesSelectResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 52; static constexpr uint16_t ESTIMATED_SIZE = 63; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_select_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; - std::string icon{}; std::vector options{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1525,14 +1460,13 @@ class ListEntitiesSelectResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class SelectStateResponse : public ProtoMessage { +class SelectStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 53; static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "select_state_response"; } #endif - uint32_t key{0}; std::string state{}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -1565,23 +1499,16 @@ class SelectCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class ListEntitiesSirenResponse : public ProtoMessage { +class ListEntitiesSirenResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 55; static constexpr uint16_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_siren_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; - std::string icon{}; - bool disabled_by_default{false}; std::vector tones{}; bool supports_duration{false}; bool supports_volume{false}; - enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1593,14 +1520,13 @@ class ListEntitiesSirenResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class SirenStateResponse : public ProtoMessage { +class SirenStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 56; static constexpr uint16_t ESTIMATED_SIZE = 7; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "siren_state_response"; } #endif - uint32_t key{0}; bool state{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -1639,20 +1565,13 @@ class SirenCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesLockResponse : public ProtoMessage { +class ListEntitiesLockResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 58; static constexpr uint16_t ESTIMATED_SIZE = 60; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_lock_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; bool assumed_state{false}; bool supports_open{false}; bool requires_code{false}; @@ -1668,14 +1587,13 @@ class ListEntitiesLockResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class LockStateResponse : public ProtoMessage { +class LockStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 59; static constexpr uint16_t ESTIMATED_SIZE = 7; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "lock_state_response"; } #endif - uint32_t key{0}; enums::LockState state{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -1709,20 +1627,13 @@ class LockCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesButtonResponse : public ProtoMessage { +class ListEntitiesButtonResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 61; static constexpr uint16_t ESTIMATED_SIZE = 54; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_button_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -1769,20 +1680,13 @@ class MediaPlayerSupportedFormat : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesMediaPlayerResponse : public ProtoMessage { +class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 63; static constexpr uint16_t ESTIMATED_SIZE = 81; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_media_player_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; bool supports_pause{false}; std::vector supported_formats{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1796,14 +1700,13 @@ class ListEntitiesMediaPlayerResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class MediaPlayerStateResponse : public ProtoMessage { +class MediaPlayerStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 64; static constexpr uint16_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "media_player_state_response"; } #endif - uint32_t key{0}; enums::MediaPlayerState state{}; float volume{0.0f}; bool muted{false}; @@ -2653,20 +2556,13 @@ class VoiceAssistantSetConfiguration : public ProtoMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class ListEntitiesAlarmControlPanelResponse : public ProtoMessage { +class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 94; static constexpr uint16_t ESTIMATED_SIZE = 53; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_alarm_control_panel_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; uint32_t supported_features{0}; bool requires_code{false}; bool requires_code_to_arm{false}; @@ -2681,14 +2577,13 @@ class ListEntitiesAlarmControlPanelResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class AlarmControlPanelStateResponse : public ProtoMessage { +class AlarmControlPanelStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 95; static constexpr uint16_t ESTIMATED_SIZE = 7; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "alarm_control_panel_state_response"; } #endif - uint32_t key{0}; enums::AlarmControlPanelState state{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -2721,20 +2616,13 @@ class AlarmControlPanelCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesTextResponse : public ProtoMessage { +class ListEntitiesTextResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 97; static constexpr uint16_t ESTIMATED_SIZE = 64; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_text_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; uint32_t min_length{0}; uint32_t max_length{0}; std::string pattern{}; @@ -2750,14 +2638,13 @@ class ListEntitiesTextResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class TextStateResponse : public ProtoMessage { +class TextStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 98; static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "text_state_response"; } #endif - uint32_t key{0}; std::string state{}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -2790,20 +2677,13 @@ class TextCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class ListEntitiesDateResponse : public ProtoMessage { +class ListEntitiesDateResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 100; static constexpr uint16_t ESTIMATED_SIZE = 45; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_date_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2815,14 +2695,13 @@ class ListEntitiesDateResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class DateStateResponse : public ProtoMessage { +class DateStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 101; static constexpr uint16_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "date_state_response"; } #endif - uint32_t key{0}; bool missing_state{false}; uint32_t year{0}; uint32_t month{0}; @@ -2858,20 +2737,13 @@ class DateCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesTimeResponse : public ProtoMessage { +class ListEntitiesTimeResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 103; static constexpr uint16_t ESTIMATED_SIZE = 45; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_time_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2883,14 +2755,13 @@ class ListEntitiesTimeResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class TimeStateResponse : public ProtoMessage { +class TimeStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 104; static constexpr uint16_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "time_state_response"; } #endif - uint32_t key{0}; bool missing_state{false}; uint32_t hour{0}; uint32_t minute{0}; @@ -2926,20 +2797,13 @@ class TimeCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesEventResponse : public ProtoMessage { +class ListEntitiesEventResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 107; static constexpr uint16_t ESTIMATED_SIZE = 72; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_event_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; std::string device_class{}; std::vector event_types{}; void encode(ProtoWriteBuffer buffer) const override; @@ -2953,14 +2817,13 @@ class ListEntitiesEventResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class EventResponse : public ProtoMessage { +class EventResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 108; static constexpr uint16_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "event_response"; } #endif - uint32_t key{0}; std::string event_type{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -2972,20 +2835,13 @@ class EventResponse : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class ListEntitiesValveResponse : public ProtoMessage { +class ListEntitiesValveResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 109; static constexpr uint16_t ESTIMATED_SIZE = 60; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_valve_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; std::string device_class{}; bool assumed_state{false}; bool supports_position{false}; @@ -3001,14 +2857,13 @@ class ListEntitiesValveResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ValveStateResponse : public ProtoMessage { +class ValveStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 110; static constexpr uint16_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "valve_state_response"; } #endif - uint32_t key{0}; float position{0.0f}; enums::ValveOperation current_operation{}; void encode(ProtoWriteBuffer buffer) const override; @@ -3042,20 +2897,13 @@ class ValveCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesDateTimeResponse : public ProtoMessage { +class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 112; static constexpr uint16_t ESTIMATED_SIZE = 45; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_date_time_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -3067,14 +2915,13 @@ class ListEntitiesDateTimeResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class DateTimeStateResponse : public ProtoMessage { +class DateTimeStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 113; static constexpr uint16_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "date_time_state_response"; } #endif - uint32_t key{0}; bool missing_state{false}; uint32_t epoch_seconds{0}; void encode(ProtoWriteBuffer buffer) const override; @@ -3105,20 +2952,13 @@ class DateTimeCommandRequest : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; }; -class ListEntitiesUpdateResponse : public ProtoMessage { +class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 116; static constexpr uint16_t ESTIMATED_SIZE = 54; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_update_response"; } #endif - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string unique_id{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -3131,14 +2971,13 @@ class ListEntitiesUpdateResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class UpdateStateResponse : public ProtoMessage { +class UpdateStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 117; static constexpr uint16_t ESTIMATED_SIZE = 61; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "update_state_response"; } #endif - uint32_t key{0}; bool missing_state{false}; bool in_progress{false}; bool has_progress{false}; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index d634be98c4..24b6bef843 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -848,7 +848,10 @@ def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int: return total_size -def build_message_type(desc: descriptor.DescriptorProto) -> tuple[str, str]: +def build_message_type( + desc: descriptor.DescriptorProto, + base_class_fields: dict[str, list[descriptor.FieldDescriptorProto]] = None, +) -> tuple[str, str]: public_content: list[str] = [] protected_content: list[str] = [] decode_varint: list[str] = [] @@ -859,6 +862,12 @@ def build_message_type(desc: descriptor.DescriptorProto) -> tuple[str, str]: dump: list[str] = [] size_calc: list[str] = [] + # Check if this message has a base class + base_class = get_base_class(desc) + common_field_names = set() + if base_class and base_class_fields and base_class in base_class_fields: + common_field_names = {f.name for f in base_class_fields[base_class]} + # Get message ID if it's a service message message_id: int | None = get_opt(desc, pb.id) @@ -886,8 +895,14 @@ def build_message_type(desc: descriptor.DescriptorProto) -> tuple[str, str]: ti = RepeatedTypeInfo(field) else: ti = TYPE_INFO[field.type](field) - protected_content.extend(ti.protected_content) - public_content.extend(ti.public_content) + + # 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: + protected_content.extend(ti.protected_content) + public_content.extend(ti.public_content) + + # Always include encode/decode logic for all fields encode.append(ti.encode_content) size_calc.append(ti.get_size_calculation(f"this->{ti.field_name}")) @@ -1001,7 +1016,10 @@ def build_message_type(desc: descriptor.DescriptorProto) -> tuple[str, str]: prot += "#endif\n" public_content.append(prot) - out = f"class {desc.name} : public ProtoMessage {{\n" + if base_class: + out = f"class {desc.name} : public {base_class} {{\n" + else: + out = f"class {desc.name} : public ProtoMessage {{\n" out += " public:\n" out += indent("\n".join(public_content)) + "\n" out += "\n" @@ -1033,6 +1051,132 @@ def get_opt( return desc.options.Extensions[opt] +def get_base_class(desc: descriptor.DescriptorProto) -> str | None: + """Get the base_class option from a message descriptor.""" + if not desc.options.HasExtension(pb.base_class): + return None + return desc.options.Extensions[pb.base_class] + + +def collect_messages_by_base_class( + messages: list[descriptor.DescriptorProto], +) -> dict[str, list[descriptor.DescriptorProto]]: + """Group messages by their base_class option.""" + base_class_groups = {} + + for msg in messages: + base_class = get_base_class(msg) + if base_class: + if base_class not in base_class_groups: + base_class_groups[base_class] = [] + base_class_groups[base_class].append(msg) + + return base_class_groups + + +def find_common_fields( + messages: list[descriptor.DescriptorProto], +) -> list[descriptor.FieldDescriptorProto]: + """Find fields that are common to all messages in the list.""" + if not messages: + return [] + + # Start with fields from the first message + first_msg_fields = {field.name: field for field in messages[0].field} + common_fields = [] + + # Check each field to see if it exists in all messages with same type + # Field numbers can vary between messages - derived classes handle the mapping + for field_name, field in first_msg_fields.items(): + is_common = True + + for msg in messages[1:]: + found = False + for other_field in msg.field: + if ( + other_field.name == field_name + and other_field.type == field.type + and other_field.label == field.label + ): + found = True + break + + if not found: + is_common = False + break + + if is_common: + common_fields.append(field) + + # Sort by field number to maintain order + common_fields.sort(key=lambda f: f.number) + return common_fields + + +def build_base_class( + base_class_name: str, + common_fields: list[descriptor.FieldDescriptorProto], +) -> tuple[str, str]: + """Build the base class definition and implementation.""" + public_content = [] + protected_content = [] + + # For base classes, we only declare the fields but don't handle encode/decode + # The derived classes will handle encoding/decoding with their specific field numbers + for field in common_fields: + if field.label == 3: # repeated + ti = RepeatedTypeInfo(field) + else: + ti = TYPE_INFO[field.type](field) + + # Only add field declarations, not encode/decode logic + protected_content.extend(ti.protected_content) + public_content.extend(ti.public_content) + + # Build header + out = f"class {base_class_name} : public ProtoMessage {{\n" + out += " public:\n" + + # Add destructor with override + public_content.insert(0, f"~{base_class_name}() override = default;") + + # Base classes don't implement encode/decode/calculate_size + # Derived classes handle these with their specific field numbers + cpp = "" + + out += indent("\n".join(public_content)) + "\n" + out += "\n" + out += " protected:\n" + out += indent("\n".join(protected_content)) + if protected_content: + out += "\n" + out += "};\n" + + # No implementation needed for base classes + + return out, cpp + + +def generate_base_classes( + base_class_groups: dict[str, list[descriptor.DescriptorProto]], +) -> tuple[str, str]: + """Generate all base classes.""" + all_headers = [] + all_cpp = [] + + for base_class_name, messages in base_class_groups.items(): + # Find common fields + common_fields = find_common_fields(messages) + + if common_fields: + # Generate base class + header, cpp = build_base_class(base_class_name, common_fields) + all_headers.append(header) + all_cpp.append(cpp) + + return "\n".join(all_headers), "\n".join(all_cpp) + + def build_service_message_type( mt: descriptor.DescriptorProto, ) -> tuple[str, str] | None: @@ -1134,8 +1278,25 @@ def main() -> None: mt = file.message_type + # Collect messages by base class + base_class_groups = collect_messages_by_base_class(mt) + + # Find common fields for each base class + base_class_fields = {} + for base_class_name, messages in base_class_groups.items(): + common_fields = find_common_fields(messages) + if common_fields: + base_class_fields[base_class_name] = common_fields + + # Generate base classes + if base_class_fields: + base_headers, base_cpp = generate_base_classes(base_class_groups) + content += base_headers + cpp += base_cpp + + # Generate message types with base class information for m in mt: - s, c = build_message_type(m) + s, c = build_message_type(m, base_class_fields) content += s cpp += c From 28d11553e045854e05e2a0b61e35105e4f8ffddd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Jun 2025 16:33:38 -0500 Subject: [PATCH 033/293] Reduce Component blocking threshold memory usage by 2 bytes per component (#9081) --- esphome/core/component.cpp | 13 ++++++++++--- esphome/core/component.h | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index dae99a0d22..03c44599e2 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -1,6 +1,7 @@ #include "esphome/core/component.h" #include +#include #include #include "esphome/core/application.h" #include "esphome/core/hal.h" @@ -41,8 +42,8 @@ const uint8_t STATUS_LED_OK = 0x00; const uint8_t STATUS_LED_WARNING = 0x04; // Bit 2 const uint8_t STATUS_LED_ERROR = 0x08; // Bit 3 -const uint32_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning -const uint32_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again +const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning +const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again uint32_t global_state = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -122,7 +123,13 @@ const char *Component::get_component_source() const { } bool Component::should_warn_of_blocking(uint32_t blocking_time) { if (blocking_time > this->warn_if_blocking_over_) { - this->warn_if_blocking_over_ = blocking_time + WARN_IF_BLOCKING_INCREMENT_MS; + // Prevent overflow when adding increment - if we're about to overflow, just max out + if (blocking_time + WARN_IF_BLOCKING_INCREMENT_MS < blocking_time || + blocking_time + WARN_IF_BLOCKING_INCREMENT_MS > std::numeric_limits::max()) { + this->warn_if_blocking_over_ = std::numeric_limits::max(); + } else { + this->warn_if_blocking_over_ = static_cast(blocking_time + WARN_IF_BLOCKING_INCREMENT_MS); + } return true; } return false; diff --git a/esphome/core/component.h b/esphome/core/component.h index 7ad4a5e496..d05a965034 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -65,7 +65,7 @@ extern const uint8_t STATUS_LED_ERROR; enum class RetryResult { DONE, RETRY }; -extern const uint32_t WARN_IF_BLOCKING_OVER_MS; +extern const uint16_t WARN_IF_BLOCKING_OVER_MS; class Component { public: @@ -318,7 +318,7 @@ class Component { uint8_t component_state_{0x00}; float setup_priority_override_{NAN}; const char *component_source_{nullptr}; - uint32_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; + uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s) std::string error_message_{}; }; From c17a3b6fccb5ea0b782ef06cc6c9508702ec471e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Jun 2025 16:34:37 -0500 Subject: [PATCH 034/293] Reduce Component memory usage by 20 bytes per component (#9080) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/core/component.cpp | 3 ++- esphome/core/component.h | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 03c44599e2..0a4606074a 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -85,7 +85,8 @@ void Component::call_setup() { this->setup(); } void Component::call_dump_config() { this->dump_config(); if (this->is_failed()) { - ESP_LOGE(TAG, " Component %s is marked FAILED: %s", this->get_component_source(), this->error_message_.c_str()); + ESP_LOGE(TAG, " Component %s is marked FAILED: %s", this->get_component_source(), + this->error_message_ ? this->error_message_ : "unspecified"); } } diff --git a/esphome/core/component.h b/esphome/core/component.h index d05a965034..f77d40ae35 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -319,7 +319,7 @@ class Component { float setup_priority_override_{NAN}; const char *component_source_{nullptr}; uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s) - std::string error_message_{}; + const char *error_message_{nullptr}; }; /** This class simplifies creating components that periodically check a state. From 882bfc79c7884e457e5d642fa182078d5ac257da Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 16 Jun 2025 12:55:23 +1200 Subject: [PATCH 035/293] Remove ``std::`` prefix as not all platforms have access yet. (#9095) --- esphome/components/sensor/sensor.cpp | 2 +- esphome/components/text_sensor/text_sensor.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 3be0df9963..6d6cff0400 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -54,7 +54,7 @@ void Sensor::publish_state(float state) { void Sensor::add_on_state_callback(std::function &&callback) { this->callback_.add(std::move(callback)); } void Sensor::add_on_raw_state_callback(std::function &&callback) { if (!this->raw_callback_) { - this->raw_callback_ = std::make_unique>(); + this->raw_callback_ = make_unique>(); } this->raw_callback_->add(std::move(callback)); } diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 91cb320782..c57e0ffefb 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -56,7 +56,7 @@ void TextSensor::add_on_state_callback(std::function callback } void TextSensor::add_on_raw_state_callback(std::function callback) { if (!this->raw_callback_) { - this->raw_callback_ = std::make_unique>(); + this->raw_callback_ = make_unique>(); } this->raw_callback_->add(std::move(callback)); } From c4f7c2d259069f38367765856c9ab618049dfb21 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:13:14 +1200 Subject: [PATCH 036/293] [ruff] Apply various ruff suggestions (#8947) --- esphome/components/canbus/__init__.py | 1 + esphome/components/cm1106/sensor.py | 4 ++-- esphome/components/mcp4461/__init__.py | 2 +- esphome/components/mcp4461/output/__init__.py | 5 +++-- esphome/components/scd30/sensor.py | 4 ++-- esphome/components/scd4x/sensor.py | 8 ++++---- esphome/components/sdp3x/sensor.py | 2 +- 7 files changed, 14 insertions(+), 12 deletions(-) diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index 6867177795..cdb57fd481 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -1,4 +1,5 @@ import re + from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv diff --git a/esphome/components/cm1106/sensor.py b/esphome/components/cm1106/sensor.py index 1b8ac14fbe..1d95bcc666 100644 --- a/esphome/components/cm1106/sensor.py +++ b/esphome/components/cm1106/sensor.py @@ -1,10 +1,10 @@ """CM1106 Sensor component for ESPHome.""" -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id +import esphome.codegen as cg from esphome.components import sensor, uart +import esphome.config_validation as cv from esphome.const import ( CONF_CO2, CONF_ID, diff --git a/esphome/components/mcp4461/__init__.py b/esphome/components/mcp4461/__init__.py index 1764629ff3..f3ef6f4917 100644 --- a/esphome/components/mcp4461/__init__.py +++ b/esphome/components/mcp4461/__init__.py @@ -1,6 +1,6 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import i2c +import esphome.config_validation as cv from esphome.const import CONF_ID CODEOWNERS = ["@p1ngb4ck"] diff --git a/esphome/components/mcp4461/output/__init__.py b/esphome/components/mcp4461/output/__init__.py index ba59f97643..02bdbefed5 100644 --- a/esphome/components/mcp4461/output/__init__.py +++ b/esphome/components/mcp4461/output/__init__.py @@ -1,8 +1,9 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import output +import esphome.config_validation as cv from esphome.const import CONF_CHANNEL, CONF_ID, CONF_INITIAL_VALUE -from .. import Mcp4461Component, CONF_MCP4461_ID, mcp4461_ns + +from .. import CONF_MCP4461_ID, Mcp4461Component, mcp4461_ns DEPENDENCIES = ["mcp4461"] diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index fb3ad713bb..f341d2a47b 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -3,6 +3,8 @@ import esphome.codegen as cg from esphome.components import i2c, sensirion_common, sensor import esphome.config_validation as cv from esphome.const import ( + CONF_AMBIENT_PRESSURE_COMPENSATION, + CONF_AUTOMATIC_SELF_CALIBRATION, CONF_CO2, CONF_HUMIDITY, CONF_ID, @@ -18,8 +20,6 @@ from esphome.const import ( UNIT_CELSIUS, UNIT_PARTS_PER_MILLION, UNIT_PERCENT, - CONF_AUTOMATIC_SELF_CALIBRATION, - CONF_AMBIENT_PRESSURE_COMPENSATION, ) DEPENDENCIES = ["i2c"] diff --git a/esphome/components/scd4x/sensor.py b/esphome/components/scd4x/sensor.py index f753f54c3b..fc859d63b8 100644 --- a/esphome/components/scd4x/sensor.py +++ b/esphome/components/scd4x/sensor.py @@ -4,9 +4,13 @@ import esphome.codegen as cg from esphome.components import i2c, sensirion_common, sensor import esphome.config_validation as cv from esphome.const import ( + CONF_AMBIENT_PRESSURE_COMPENSATION, + CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE, + CONF_AUTOMATIC_SELF_CALIBRATION, CONF_CO2, CONF_HUMIDITY, CONF_ID, + CONF_MEASUREMENT_MODE, CONF_TEMPERATURE, CONF_TEMPERATURE_OFFSET, CONF_VALUE, @@ -20,10 +24,6 @@ from esphome.const import ( UNIT_CELSIUS, UNIT_PARTS_PER_MILLION, UNIT_PERCENT, - CONF_AUTOMATIC_SELF_CALIBRATION, - CONF_AMBIENT_PRESSURE_COMPENSATION, - CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE, - CONF_MEASUREMENT_MODE, ) CODEOWNERS = ["@sjtrny", "@martgras"] diff --git a/esphome/components/sdp3x/sensor.py b/esphome/components/sdp3x/sensor.py index 7cda2779ce..169ed374ed 100644 --- a/esphome/components/sdp3x/sensor.py +++ b/esphome/components/sdp3x/sensor.py @@ -2,10 +2,10 @@ import esphome.codegen as cg from esphome.components import i2c, sensirion_common, sensor import esphome.config_validation as cv from esphome.const import ( + CONF_MEASUREMENT_MODE, DEVICE_CLASS_PRESSURE, STATE_CLASS_MEASUREMENT, UNIT_HECTOPASCAL, - CONF_MEASUREMENT_MODE, ) DEPENDENCIES = ["i2c"] From 68ef9cb3dc924d15068ef94873137b5d9bd2bb70 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 16 Jun 2025 02:36:49 -0500 Subject: [PATCH 037/293] [i2s_audio] Add ``dump_config`` methods, shorten log messages (#9099) --- esphome/components/i2s_audio/i2s_audio.cpp | 2 +- .../microphone/i2s_audio_microphone.cpp | 51 +++++++++++-------- .../microphone/i2s_audio_microphone.h | 1 + .../i2s_audio/speaker/i2s_audio_speaker.cpp | 38 ++++++++++---- .../i2s_audio/speaker/i2s_audio_speaker.h | 1 + 5 files changed, 61 insertions(+), 32 deletions(-) diff --git a/esphome/components/i2s_audio/i2s_audio.cpp b/esphome/components/i2s_audio/i2s_audio.cpp index 2de3f1d9f8..0f2995b4bd 100644 --- a/esphome/components/i2s_audio/i2s_audio.cpp +++ b/esphome/components/i2s_audio/i2s_audio.cpp @@ -18,7 +18,7 @@ void I2SAudioComponent::setup() { static i2s_port_t next_port_num = I2S_NUM_0; if (next_port_num >= I2S_NUM_MAX) { - ESP_LOGE(TAG, "Too many I2S Audio components"); + ESP_LOGE(TAG, "Too many components"); this->mark_failed(); return; } diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 52d0ae34fb..2cd004ffaa 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -45,7 +45,7 @@ void I2SAudioMicrophone::setup() { #if SOC_I2S_SUPPORTS_ADC if (this->adc_) { if (this->parent_->get_port() != I2S_NUM_0) { - ESP_LOGE(TAG, "Internal ADC only works on I2S0!"); + ESP_LOGE(TAG, "Internal ADC only works on I2S0"); this->mark_failed(); return; } @@ -55,7 +55,7 @@ void I2SAudioMicrophone::setup() { { if (this->pdm_) { if (this->parent_->get_port() != I2S_NUM_0) { - ESP_LOGE(TAG, "PDM only works on I2S0!"); + ESP_LOGE(TAG, "PDM only works on I2S0"); this->mark_failed(); return; } @@ -64,14 +64,14 @@ void I2SAudioMicrophone::setup() { this->active_listeners_semaphore_ = xSemaphoreCreateCounting(MAX_LISTENERS, MAX_LISTENERS); if (this->active_listeners_semaphore_ == nullptr) { - ESP_LOGE(TAG, "Failed to create semaphore"); + ESP_LOGE(TAG, "Creating semaphore failed"); this->mark_failed(); return; } this->event_group_ = xEventGroupCreate(); if (this->event_group_ == nullptr) { - ESP_LOGE(TAG, "Failed to create event group"); + ESP_LOGE(TAG, "Creating event group failed"); this->mark_failed(); return; } @@ -79,6 +79,15 @@ void I2SAudioMicrophone::setup() { this->configure_stream_settings_(); } +void I2SAudioMicrophone::dump_config() { + ESP_LOGCONFIG(TAG, + "Microphone:\n" + " Pin: %d\n" + " PDM: %s\n" + " DC offset correction: %s", + static_cast(this->din_pin_), YESNO(this->pdm_), YESNO(this->correct_dc_offset_)); +} + void I2SAudioMicrophone::configure_stream_settings_() { uint8_t channel_count = 1; #ifdef USE_I2S_LEGACY @@ -151,7 +160,7 @@ bool I2SAudioMicrophone::start_driver_() { config.mode = (i2s_mode_t) (config.mode | I2S_MODE_ADC_BUILT_IN); err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); if (err != ESP_OK) { - ESP_LOGE(TAG, "Error installing I2S driver: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error installing driver: %s", esp_err_to_name(err)); return false; } @@ -174,7 +183,7 @@ bool I2SAudioMicrophone::start_driver_() { err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); if (err != ESP_OK) { - ESP_LOGE(TAG, "Error installing I2S driver: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error installing driver: %s", esp_err_to_name(err)); return false; } @@ -183,7 +192,7 @@ bool I2SAudioMicrophone::start_driver_() { err = i2s_set_pin(this->parent_->get_port(), &pin_config); if (err != ESP_OK) { - ESP_LOGE(TAG, "Error setting I2S pin: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error setting pin: %s", esp_err_to_name(err)); return false; } } @@ -198,7 +207,7 @@ bool I2SAudioMicrophone::start_driver_() { /* Allocate a new RX channel and get the handle of this channel */ err = i2s_new_channel(&chan_cfg, NULL, &this->rx_handle_); if (err != ESP_OK) { - ESP_LOGE(TAG, "Error creating new I2S channel: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error creating channel: %s", esp_err_to_name(err)); return false; } @@ -270,14 +279,14 @@ bool I2SAudioMicrophone::start_driver_() { err = i2s_channel_init_std_mode(this->rx_handle_, &std_cfg); } if (err != ESP_OK) { - ESP_LOGE(TAG, "Error initializing I2S channel: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error initializing channel: %s", esp_err_to_name(err)); return false; } /* Before reading data, start the RX channel first */ i2s_channel_enable(this->rx_handle_); if (err != ESP_OK) { - ESP_LOGE(TAG, "Error enabling I2S Microphone: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "Enabling failed: %s", esp_err_to_name(err)); return false; } #endif @@ -304,29 +313,29 @@ void I2SAudioMicrophone::stop_driver_() { if (this->adc_) { err = i2s_adc_disable(this->parent_->get_port()); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error disabling ADC - it may not have started: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "Error disabling ADC: %s", esp_err_to_name(err)); } } #endif err = i2s_stop(this->parent_->get_port()); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error stopping I2S microphone - it may not have started: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "Error stopping: %s", esp_err_to_name(err)); } err = i2s_driver_uninstall(this->parent_->get_port()); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error uninstalling I2S driver - it may not have started: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "Error uninstalling driver: %s", esp_err_to_name(err)); } #else if (this->rx_handle_ != nullptr) { /* Have to stop the channel before deleting it */ err = i2s_channel_disable(this->rx_handle_); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error stopping I2S microphone - it may not have started: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "Error stopping: %s", esp_err_to_name(err)); } /* If the handle is not needed any more, delete it to release the channel resources */ err = i2s_del_channel(this->rx_handle_); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error deleting I2S channel - it may not have started: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "Error deleting channel: %s", esp_err_to_name(err)); } this->rx_handle_ = nullptr; } @@ -403,7 +412,7 @@ size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_w // Ignore ESP_ERR_TIMEOUT if ticks_to_wait = 0, as it will read the data on the next call if (!this->status_has_warning()) { // Avoid spamming the logs with the error message if its repeated - ESP_LOGW(TAG, "Error reading from I2S microphone: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "Read error: %s", esp_err_to_name(err)); } this->status_set_warning(); return 0; @@ -431,19 +440,19 @@ void I2SAudioMicrophone::loop() { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); if (event_group_bits & MicrophoneEventGroupBits::TASK_STARTING) { - ESP_LOGD(TAG, "Task started, attempting to allocate buffer"); + ESP_LOGV(TAG, "Task started, attempting to allocate buffer"); xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_STARTING); } if (event_group_bits & MicrophoneEventGroupBits::TASK_RUNNING) { - ESP_LOGD(TAG, "Task is running and reading data"); + ESP_LOGV(TAG, "Task is running and reading data"); xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_RUNNING); this->state_ = microphone::STATE_RUNNING; } if ((event_group_bits & MicrophoneEventGroupBits::TASK_STOPPED)) { - ESP_LOGD(TAG, "Task finished, freeing resources and uninstalling I2S driver"); + ESP_LOGV(TAG, "Task finished, freeing resources and uninstalling driver"); vTaskDelete(this->task_handle_); this->task_handle_ = nullptr; @@ -473,7 +482,7 @@ void I2SAudioMicrophone::loop() { } if (!this->start_driver_()) { - this->status_momentary_error("I2S driver failed to start, unloading it and attempting again in 1 second", 1000); + this->status_momentary_error("Driver failed to start; retrying in 1 second", 1000); this->stop_driver_(); // Stop/frees whatever possibly started break; } @@ -483,7 +492,7 @@ void I2SAudioMicrophone::loop() { &this->task_handle_); if (this->task_handle_ == nullptr) { - this->status_momentary_error("Task failed to start, attempting again in 1 second", 1000); + this->status_momentary_error("Task failed to start, retrying in 1 second", 1000); this->stop_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt } } diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index c35f88f8ee..4c384ba963 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -18,6 +18,7 @@ namespace i2s_audio { class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, public Component { public: void setup() override; + void dump_config() override; void start() override; void stop() override; diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index f4c761ecc0..41da8a4642 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -110,29 +110,48 @@ void I2SAudioSpeaker::setup() { } } +void I2SAudioSpeaker::dump_config() { + ESP_LOGCONFIG(TAG, + "Speaker:\n" + " Pin: %d\n" + " Buffer duration: %" PRIu32, + static_cast(this->dout_pin_), this->buffer_duration_ms_); + if (this->timeout_.has_value()) { + ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 " ms", this->timeout_.value()); + } +#ifdef USE_I2S_LEGACY +#if SOC_I2S_SUPPORTS_DAC + ESP_LOGCONFIG(TAG, " Internal DAC mode: %d", static_cast(this->internal_dac_mode_)); +#endif + ESP_LOGCONFIG(TAG, " Communication format: %d", static_cast(this->i2s_comm_fmt_)); +#else + ESP_LOGCONFIG(TAG, " Communication format: %s", this->i2s_comm_fmt_.c_str()); +#endif +} + void I2SAudioSpeaker::loop() { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); if (event_group_bits & SpeakerEventGroupBits::STATE_STARTING) { - ESP_LOGD(TAG, "Starting Speaker"); + ESP_LOGD(TAG, "Starting"); this->state_ = speaker::STATE_STARTING; xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_STARTING); } if (event_group_bits & SpeakerEventGroupBits::STATE_RUNNING) { - ESP_LOGD(TAG, "Started Speaker"); + ESP_LOGD(TAG, "Started"); this->state_ = speaker::STATE_RUNNING; xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_RUNNING); this->status_clear_warning(); this->status_clear_error(); } if (event_group_bits & SpeakerEventGroupBits::STATE_STOPPING) { - ESP_LOGD(TAG, "Stopping Speaker"); + ESP_LOGD(TAG, "Stopping"); this->state_ = speaker::STATE_STOPPING; xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPING); } if (event_group_bits & SpeakerEventGroupBits::STATE_STOPPED) { if (!this->task_created_) { - ESP_LOGD(TAG, "Stopped Speaker"); + ESP_LOGD(TAG, "Stopped"); this->state_ = speaker::STATE_STOPPED; xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS); this->speaker_task_handle_ = nullptr; @@ -140,20 +159,19 @@ void I2SAudioSpeaker::loop() { } if (event_group_bits & SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START) { - this->status_set_error("Failed to start speaker task"); + this->status_set_error("Failed to start task"); xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START); } if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) { uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS; - ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(err_bit_to_esp_err(error_bits))); + ESP_LOGW(TAG, "Writing failed: %s", esp_err_to_name(err_bit_to_esp_err(error_bits))); this->status_set_warning(); } if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED) { - this->status_set_error("Failed to adjust I2S bus to match the incoming audio"); - ESP_LOGE(TAG, - "Incompatible audio format: sample rate = %" PRIu32 ", channels = %" PRIu8 ", bits per sample = %" PRIu8, + this->status_set_error("Failed to adjust bus to match incoming audio"); + ESP_LOGE(TAG, "Incompatible audio format: sample rate = %" PRIu32 ", channels = %u, bits per sample = %u", this->audio_stream_info_.get_sample_rate(), this->audio_stream_info_.get_channels(), this->audio_stream_info_.get_bits_per_sample()); } @@ -202,7 +220,7 @@ void I2SAudioSpeaker::set_mute_state(bool mute_state) { size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) { if (this->is_failed()) { - ESP_LOGE(TAG, "Cannot play audio, speaker failed to setup"); + ESP_LOGE(TAG, "Setup failed; cannot play audio"); return 0; } if (this->state_ != speaker::STATE_RUNNING && this->state_ != speaker::STATE_STARTING) { diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index b5e4b94bc4..eb2a0ae756 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -24,6 +24,7 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; } void setup() override; + void dump_config() override; void loop() override; void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; } From fa7c42511a22980f9cea9073b5ba68e8f9ce6ba3 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 17 Jun 2025 01:59:07 +0100 Subject: [PATCH 038/293] [i2s_audio] Bugfix: crashes when unlocking i2s bus multiple times (#9100) --- .../i2s_audio/microphone/i2s_audio_microphone.cpp | 12 +++++++++--- .../i2s_audio/microphone/i2s_audio_microphone.h | 1 + 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 2cd004ffaa..0477e0682d 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -136,6 +136,7 @@ bool I2SAudioMicrophone::start_driver_() { if (!this->parent_->try_lock()) { return false; // Waiting for another i2s to return lock } + this->locked_driver_ = true; esp_err_t err; #ifdef USE_I2S_LEGACY @@ -340,7 +341,10 @@ void I2SAudioMicrophone::stop_driver_() { this->rx_handle_ = nullptr; } #endif - this->parent_->unlock(); + if (this->locked_driver_) { + this->parent_->unlock(); + this->locked_driver_ = false; + } } void I2SAudioMicrophone::mic_task(void *params) { @@ -482,7 +486,8 @@ void I2SAudioMicrophone::loop() { } if (!this->start_driver_()) { - this->status_momentary_error("Driver failed to start; retrying in 1 second", 1000); + ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second"); + this->status_momentary_error("driver_fail", 1000); this->stop_driver_(); // Stop/frees whatever possibly started break; } @@ -492,7 +497,8 @@ void I2SAudioMicrophone::loop() { &this->task_handle_); if (this->task_handle_ == nullptr) { - this->status_momentary_error("Task failed to start, retrying in 1 second", 1000); + ESP_LOGE(TAG, "Task failed to start, retrying in 1 second"); + this->status_momentary_error("task_fail", 1000); this->stop_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt } } diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index 4c384ba963..5f66f2e962 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -81,6 +81,7 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub bool pdm_{false}; bool correct_dc_offset_; + bool locked_driver_{false}; int32_t dc_offset_{0}; }; From 738ad8e9d383bce12285bf8e6b07dce41d5aef31 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 17 Jun 2025 12:30:09 +1000 Subject: [PATCH 039/293] [spi] Cater for non-word-aligned buffers on esp8266 (#9108) --- esphome/components/spi/spi_arduino.cpp | 31 ++++++++++++++++++-------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/esphome/components/spi/spi_arduino.cpp b/esphome/components/spi/spi_arduino.cpp index f7fe523a33..432f7cf2cd 100644 --- a/esphome/components/spi/spi_arduino.cpp +++ b/esphome/components/spi/spi_arduino.cpp @@ -3,7 +3,6 @@ namespace esphome { namespace spi { - #ifdef USE_ARDUINO static const char *const TAG = "spi-esp-arduino"; @@ -38,17 +37,31 @@ class SPIDelegateHw : public SPIDelegate { void write16(uint16_t data) override { this->channel_->transfer16(data); } -#ifdef USE_RP2040 void write_array(const uint8_t *ptr, size_t length) override { - // avoid overwriting the supplied buffer - uint8_t *rxbuf = new uint8_t[length]; // NOLINT(cppcoreguidelines-owning-memory) - memcpy(rxbuf, ptr, length); - this->channel_->transfer((void *) rxbuf, length); - delete[] rxbuf; // NOLINT(cppcoreguidelines-owning-memory) - } + if (length == 1) { + this->channel_->transfer(*ptr); + return; + } +#ifdef USE_RP2040 + // avoid overwriting the supplied buffer. Use vector for automatic deallocation + auto rxbuf = std::vector(length); + memcpy(rxbuf.data(), ptr, length); + this->channel_->transfer((void *) rxbuf.data(), length); +#elif defined(USE_ESP8266) + // ESP8266 SPI library requires the pointer to be word aligned, but the data may not be + // so we need to copy the data to a temporary buffer + if (reinterpret_cast(ptr) & 0x3) { + ESP_LOGVV(TAG, "SPI write buffer not word aligned, copying to temporary buffer"); + auto txbuf = std::vector(length); + memcpy(txbuf.data(), ptr, length); + this->channel_->writeBytes(txbuf.data(), length); + } else { + this->channel_->writeBytes(ptr, length); + } #else - void write_array(const uint8_t *ptr, size_t length) override { this->channel_->writeBytes(ptr, length); } + this->channel_->writeBytes(ptr, length); #endif + } void read_array(uint8_t *ptr, size_t length) override { this->channel_->transfer(ptr, length); } From b08bd0c24a237e08b0b6c00f9d104d3d66284996 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Jun 2025 22:41:18 -0400 Subject: [PATCH 040/293] Bump LibreTiny recommended version to 1.9.1 (#9110) --- esphome/components/libretiny/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 7683c29c63..28ee1e702f 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -173,9 +173,9 @@ def _notify_old_style(config): # The dev and latest branches will be at *least* this version, which is what matters. ARDUINO_VERSIONS = { - "dev": (cv.Version(1, 7, 0), "https://github.com/libretiny-eu/libretiny.git"), - "latest": (cv.Version(1, 7, 0), "libretiny"), - "recommended": (cv.Version(1, 7, 0), None), + "dev": (cv.Version(1, 9, 1), "https://github.com/libretiny-eu/libretiny.git"), + "latest": (cv.Version(1, 9, 1), "libretiny"), + "recommended": (cv.Version(1, 9, 1), None), } From 5ffe50381ae567a0954ab44ac69e72092da15ba1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 10:41:54 +0200 Subject: [PATCH 041/293] Bump docker/setup-buildx-action from 3.10.0 to 3.11.0 in the docker-actions group (#9105) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-docker.yml | 2 +- .github/workflows/release.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 5f524612ed..3bfed87237 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -49,7 +49,7 @@ jobs: with: python-version: "3.10" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.10.0 + uses: docker/setup-buildx-action@v3.11.0 - name: Set TAG run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eae01fe0b3..8239e03a99 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -99,7 +99,7 @@ jobs: python-version: "3.10" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.10.0 + uses: docker/setup-buildx-action@v3.11.0 - name: Log in to docker hub uses: docker/login-action@v3.4.0 @@ -178,7 +178,7 @@ jobs: merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.10.0 + uses: docker/setup-buildx-action@v3.11.0 - name: Log in to docker hub if: matrix.registry == 'dockerhub' From 78c8447d1e2a367afa45080679909b704bbe1a11 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 17 Jun 2025 11:47:42 -0400 Subject: [PATCH 042/293] [esp32_hall] Remove esp32_hall (#9117) --- esphome/components/esp32_hall/__init__.py | 0 esphome/components/esp32_hall/esp32_hall.cpp | 25 ------------------- esphome/components/esp32_hall/esp32_hall.h | 23 ----------------- esphome/components/esp32_hall/sensor.py | 24 ------------------ .../components/esp32_hall/test.esp32-ard.yaml | 3 --- 5 files changed, 75 deletions(-) delete mode 100644 esphome/components/esp32_hall/__init__.py delete mode 100644 esphome/components/esp32_hall/esp32_hall.cpp delete mode 100644 esphome/components/esp32_hall/esp32_hall.h delete mode 100644 esphome/components/esp32_hall/sensor.py delete mode 100644 tests/components/esp32_hall/test.esp32-ard.yaml diff --git a/esphome/components/esp32_hall/__init__.py b/esphome/components/esp32_hall/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/esphome/components/esp32_hall/esp32_hall.cpp b/esphome/components/esp32_hall/esp32_hall.cpp deleted file mode 100644 index 762497aedc..0000000000 --- a/esphome/components/esp32_hall/esp32_hall.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#ifdef USE_ESP32 -#include "esp32_hall.h" -#include "esphome/core/log.h" -#include "esphome/core/hal.h" -#include - -namespace esphome { -namespace esp32_hall { - -static const char *const TAG = "esp32_hall"; - -void ESP32HallSensor::update() { - adc1_config_width(ADC_WIDTH_BIT_12); - int value_int = hall_sensor_read(); - float value = (value_int / 4095.0f) * 10000.0f; - ESP_LOGD(TAG, "'%s': Got reading %.0f µT", this->name_.c_str(), value); - this->publish_state(value); -} -std::string ESP32HallSensor::unique_id() { return get_mac_address() + "-hall"; } -void ESP32HallSensor::dump_config() { LOG_SENSOR("", "ESP32 Hall Sensor", this); } - -} // namespace esp32_hall -} // namespace esphome - -#endif diff --git a/esphome/components/esp32_hall/esp32_hall.h b/esphome/components/esp32_hall/esp32_hall.h deleted file mode 100644 index 8db50c4667..0000000000 --- a/esphome/components/esp32_hall/esp32_hall.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" - -#ifdef USE_ESP32 - -namespace esphome { -namespace esp32_hall { - -class ESP32HallSensor : public sensor::Sensor, public PollingComponent { - public: - void dump_config() override; - - void update() override; - - std::string unique_id() override; -}; - -} // namespace esp32_hall -} // namespace esphome - -#endif diff --git a/esphome/components/esp32_hall/sensor.py b/esphome/components/esp32_hall/sensor.py deleted file mode 100644 index e7953d4b3d..0000000000 --- a/esphome/components/esp32_hall/sensor.py +++ /dev/null @@ -1,24 +0,0 @@ -import esphome.codegen as cg -from esphome.components import sensor -import esphome.config_validation as cv -from esphome.const import ICON_MAGNET, STATE_CLASS_MEASUREMENT, UNIT_MICROTESLA - -DEPENDENCIES = ["esp32"] - -esp32_hall_ns = cg.esphome_ns.namespace("esp32_hall") -ESP32HallSensor = esp32_hall_ns.class_( - "ESP32HallSensor", sensor.Sensor, cg.PollingComponent -) - -CONFIG_SCHEMA = sensor.sensor_schema( - ESP32HallSensor, - unit_of_measurement=UNIT_MICROTESLA, - icon=ICON_MAGNET, - accuracy_decimals=1, - state_class=STATE_CLASS_MEASUREMENT, -).extend(cv.polling_component_schema("60s")) - - -async def to_code(config): - var = await sensor.new_sensor(config) - await cg.register_component(var, config) diff --git a/tests/components/esp32_hall/test.esp32-ard.yaml b/tests/components/esp32_hall/test.esp32-ard.yaml deleted file mode 100644 index f8429f5aa0..0000000000 --- a/tests/components/esp32_hall/test.esp32-ard.yaml +++ /dev/null @@ -1,3 +0,0 @@ -sensor: - - platform: esp32_hall - name: ESP32 Hall Sensor From bf161f1eaa84060e29b3a275712e7599b33ebe77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Jun 2025 20:04:45 +0200 Subject: [PATCH 043/293] Resolve esphome::optional vs std::optional ambiguity in code generation (#9119) --- esphome/cpp_types.py | 4 +++- tests/component_tests/text/test_text.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index dab993f87f..a0dd62cb4e 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -29,7 +29,9 @@ Component = esphome_ns.class_("Component") ComponentPtr = Component.operator("ptr") PollingComponent = esphome_ns.class_("PollingComponent", Component) Application = esphome_ns.class_("Application") -optional = esphome_ns.class_("optional") +# Create optional with explicit namespace to avoid ambiguity with std::optional +# The generated code will use esphome::optional instead of just optional +optional = global_ns.namespace("esphome").class_("optional") arduino_json_ns = global_ns.namespace("ArduinoJson") JsonObject = arduino_json_ns.class_("JsonObject") JsonObjectConst = arduino_json_ns.class_("JsonObjectConst") diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 51fcb3d382..75f1c4b88b 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -66,5 +66,5 @@ def test_text_config_lamda_is_set(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "it_4->set_template([=]() -> optional {" in main_cpp + assert "it_4->set_template([=]() -> esphome::optional {" in main_cpp assert 'return std::string{"Hello"};' in main_cpp From cb8b0ec62e7fcfff078dfdef90d46a6007e21bdf Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 17 Jun 2025 13:05:06 -0500 Subject: [PATCH 044/293] Add intent progress event to voice assistant enum (#9103) --- esphome/components/api/api.proto | 1 + esphome/components/api/api_pb2.cpp | 2 ++ esphome/components/api/api_pb2.h | 1 + 3 files changed, 4 insertions(+) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 843b72795a..b23652a982 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1643,6 +1643,7 @@ enum VoiceAssistantEvent { VOICE_ASSISTANT_STT_VAD_END = 12; VOICE_ASSISTANT_TTS_STREAM_START = 98; VOICE_ASSISTANT_TTS_STREAM_END = 99; + VOICE_ASSISTANT_INTENT_PROGRESS = 100; } message VoiceAssistantEventData { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 415409f880..bde1824d71 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -516,6 +516,8 @@ template<> const char *proto_enum_to_string(enums::V return "VOICE_ASSISTANT_TTS_STREAM_START"; case enums::VOICE_ASSISTANT_TTS_STREAM_END: return "VOICE_ASSISTANT_TTS_STREAM_END"; + case enums::VOICE_ASSISTANT_INTENT_PROGRESS: + return "VOICE_ASSISTANT_INTENT_PROGRESS"; default: return "UNKNOWN"; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 14a1f3f353..9d270bcdc1 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -208,6 +208,7 @@ enum VoiceAssistantEvent : uint32_t { VOICE_ASSISTANT_STT_VAD_END = 12, VOICE_ASSISTANT_TTS_STREAM_START = 98, VOICE_ASSISTANT_TTS_STREAM_END = 99, + VOICE_ASSISTANT_INTENT_PROGRESS = 100, }; enum VoiceAssistantTimerEvent : uint32_t { VOICE_ASSISTANT_TIMER_STARTED = 0, From 7ed095e635ef54d69af35bc6a4f5894e366ead11 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Jun 2025 20:07:45 +0200 Subject: [PATCH 045/293] Optimize LightState memory layout (#9113) --- esphome/components/light/light_state.h | 30 +++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index acba986f24..b93823feac 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -17,7 +17,7 @@ namespace light { class LightOutput; -enum LightRestoreMode { +enum LightRestoreMode : uint8_t { LIGHT_RESTORE_DEFAULT_OFF, LIGHT_RESTORE_DEFAULT_ON, LIGHT_ALWAYS_OFF, @@ -212,12 +212,18 @@ class LightState : public EntityBase, public Component { /// Store the output to allow effects to have more access. LightOutput *output_; - /// Value for storing the index of the currently active effect. 0 if no effect is active - uint32_t active_effect_index_{}; /// The currently active transformer for this light (transition/flash). std::unique_ptr transformer_{nullptr}; - /// Whether the light value should be written in the next cycle. - bool next_write_{true}; + /// List of effects for this light. + std::vector effects_; + /// Value for storing the index of the currently active effect. 0 if no effect is active + uint32_t active_effect_index_{}; + /// Default transition length for all transitions in ms. + uint32_t default_transition_length_{}; + /// Transition length to use for flash transitions. + uint32_t flash_transition_length_{}; + /// Gamma correction factor for the light. + float gamma_correct_{}; /// Object used to store the persisted values of the light. ESPPreferenceObject rtc_; @@ -236,19 +242,13 @@ class LightState : public EntityBase, public Component { */ CallbackManager target_state_reached_callback_{}; - /// Default transition length for all transitions in ms. - uint32_t default_transition_length_{}; - /// Transition length to use for flash transitions. - uint32_t flash_transition_length_{}; - /// Gamma correction factor for the light. - float gamma_correct_{}; - /// Restore mode of the light. - LightRestoreMode restore_mode_; /// Initial state of the light. optional initial_state_{}; - /// List of effects for this light. - std::vector effects_; + /// Restore mode of the light. + LightRestoreMode restore_mode_; + /// Whether the light value should be written in the next cycle. + bool next_write_{true}; // for effects, true if a transformer (transition) is active. bool is_transformer_active_ = false; }; From 47e7988c8e73254802d22257d71d174d8721d6c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Jun 2025 20:14:03 +0200 Subject: [PATCH 046/293] Reduce Switch component memory usage by 8 bytes per instance (#9112) --- esphome/components/switch/switch.h | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index e8018ed36f..b999296564 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -21,7 +21,7 @@ const int RESTORE_MODE_PERSISTENT_MASK = 0x02; const int RESTORE_MODE_INVERTED_MASK = 0x04; const int RESTORE_MODE_DISABLED_MASK = 0x08; -enum SwitchRestoreMode { +enum SwitchRestoreMode : uint8_t { SWITCH_ALWAYS_OFF = !RESTORE_MODE_ON_MASK, SWITCH_ALWAYS_ON = RESTORE_MODE_ON_MASK, SWITCH_RESTORE_DEFAULT_OFF = RESTORE_MODE_PERSISTENT_MASK, @@ -49,12 +49,12 @@ class Switch : public EntityBase, public EntityBase_DeviceClass { */ void publish_state(bool state); - /// The current reported state of the binary sensor. - bool state; - /// Indicates whether or not state is to be retrieved from flash and how SwitchRestoreMode restore_mode{SWITCH_RESTORE_DEFAULT_OFF}; + /// The current reported state of the binary sensor. + bool state; + /** Turn this switch on. This is called by the front-end. * * For implementing switches, please override write_state. @@ -123,10 +123,16 @@ class Switch : public EntityBase, public EntityBase_DeviceClass { */ virtual void write_state(bool state) = 0; - CallbackManager state_callback_{}; - bool inverted_{false}; - Deduplicator publish_dedup_; + // Pointer first (4 bytes) ESPPreferenceObject rtc_; + + // CallbackManager (12 bytes on 32-bit - contains vector) + CallbackManager state_callback_{}; + + // Small types grouped together + Deduplicator publish_dedup_; // 2 bytes (bool has_value_ + bool last_value_) + bool inverted_{false}; // 1 byte + // Total: 3 bytes, 1 byte padding }; #define LOG_SWITCH(prefix, type, obj) log_switch((TAG), (prefix), LOG_STR_LITERAL(type), (obj)) From 43ab63455b2cf70fede3f60114fe574da9f05a4f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 17 Jun 2025 16:42:36 -0400 Subject: [PATCH 047/293] Pin libretiny to 1.9.1 (#9118) --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 77938424f9..a3c990066a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -200,7 +200,7 @@ build_unflags = ; This are common settings for the LibreTiny (all variants) using Arduino. [common:libretiny-arduino] extends = common:arduino -platform = libretiny +platform = libretiny@1.9.1 framework = arduino lib_deps = droscy/esp_wireguard@0.4.2 ; wireguard From 0bf613bd34a52df5f6002477dc3f7ddedcb2f0f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 00:08:22 +0200 Subject: [PATCH 048/293] Bump ruff from 0.11.13 to 0.12.0 (#9120) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 2c2549c64b..8b42b9347c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.3.7 flake8==7.2.0 # also change in .pre-commit-config.yaml when updating -ruff==0.11.13 # also change in .pre-commit-config.yaml when updating +ruff==0.12.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pre-commit From 3bc5db4fd7673e64172c56a5fa11e62f0d8d8c25 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Jun 2025 00:54:45 +0200 Subject: [PATCH 049/293] Bump ruff in pre-commit to 0.12.0 (#9121) --- .pre-commit-config.yaml | 2 +- esphome/components/bme680/sensor.py | 2 +- pyproject.toml | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d55c00eea7..634c474571 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.10 + rev: v0.12.0 hooks: # Run the linter. - id: ruff diff --git a/esphome/components/bme680/sensor.py b/esphome/components/bme680/sensor.py index abdf6d3969..f41aefcec3 100644 --- a/esphome/components/bme680/sensor.py +++ b/esphome/components/bme680/sensor.py @@ -12,8 +12,8 @@ from esphome.const import ( CONF_OVERSAMPLING, CONF_PRESSURE, CONF_TEMPERATURE, - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, ICON_GAS_CYLINDER, STATE_CLASS_MEASUREMENT, diff --git a/pyproject.toml b/pyproject.toml index 3bec607150..97b0df9eff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,10 +120,12 @@ select = [ ignore = [ "E501", # line too long + "PLC0415", # `import` should be at the top-level of a file "PLR0911", # Too many return statements ({returns} > {max_returns}) "PLR0912", # Too many branches ({branches} > {max_branches}) "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) "PLR0915", # Too many statements ({statements} > {max_statements}) + "PLW1641", # Object does not implement `__hash__` method "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target "UP038", # https://github.com/astral-sh/ruff/issues/7871 https://github.com/astral-sh/ruff/pull/16681 From 53496a1ecdd1a94926b956beed45fdc5b0374991 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Jun 2025 00:15:26 -0400 Subject: [PATCH 050/293] [heatpumpir] Bump HeatpumpIR to 1.0.35 (#9123) --- esphome/components/heatpumpir/climate.py | 2 +- platformio.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index c0eb8db4b3..9e5a2bf45c 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -125,6 +125,6 @@ async def to_code(config): cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) - cg.add_library("tonia/HeatpumpIR", "1.0.32") + cg.add_library("tonia/HeatpumpIR", "1.0.35") if CORE.is_libretiny: CORE.add_platformio_option("lib_ignore", "IRremoteESP8266") diff --git a/platformio.ini b/platformio.ini index a3c990066a..4aa947ddc7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -72,7 +72,7 @@ lib_deps = glmnet/Dsmr@0.7 ; dsmr rweather/Crypto@0.4.0 ; dsmr dudanov/MideaUART@1.1.9 ; midea - tonia/HeatpumpIR@1.0.32 ; heatpumpir + tonia/HeatpumpIR@1.0.35 ; heatpumpir build_flags = ${common.build_flags} -DUSE_ARDUINO From fd3c22945bef91723252002b23d239926e30e30a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Jun 2025 00:18:23 -0400 Subject: [PATCH 051/293] [i2s_audio] Bump esphome/ESP32-audioI2S to 2.3.0 (#9124) --- esphome/components/i2s_audio/media_player/__init__.py | 2 +- platformio.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index f7ef134803..8797d13e7c 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -116,5 +116,5 @@ async def to_code(config): cg.add_library("WiFiClientSecure", None) cg.add_library("HTTPClient", None) - cg.add_library("esphome/ESP32-audioI2S", "2.2.0") + cg.add_library("esphome/ESP32-audioI2S", "2.3.0") cg.add_build_flag("-DAUDIO_NO_SD_FS") diff --git a/platformio.ini b/platformio.ini index 4aa947ddc7..58e0b77c07 100644 --- a/platformio.ini +++ b/platformio.ini @@ -135,7 +135,7 @@ lib_deps = HTTPClient ; http_request,nextion (Arduino built-in) ESPmDNS ; mdns (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) - esphome/ESP32-audioI2S@2.2.0 ; i2s_audio + esphome/ESP32-audioI2S@2.3.0 ; i2s_audio droscy/esp_wireguard@0.4.2 ; wireguard esphome/esp-audio-libs@1.1.4 ; audio From 2e534ce41ec8bd6da77746f0517302fb58201a8f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Jun 2025 11:49:25 +0200 Subject: [PATCH 052/293] Reduce CPU overhead by allowing components to disable their loop() (#9089) --- esphome/components/anova/anova.cpp | 6 +- esphome/components/bedjet/bedjet_hub.cpp | 6 +- .../bedjet/climate/bedjet_climate.cpp | 6 +- .../ble_client/sensor/ble_rssi_sensor.cpp | 6 +- .../ble_client/sensor/ble_sensor.cpp | 6 +- .../text_sensor/ble_text_sensor.cpp | 6 +- .../captive_portal/captive_portal.cpp | 9 +- .../captive_portal/captive_portal.h | 5 +- .../esp32_ble_client/ble_client_base.cpp | 17 +- .../esp32_ble_client/ble_client_base.h | 2 + .../esp32_improv/esp32_improv_component.cpp | 3 + .../components/online_image/online_image.cpp | 4 + esphome/components/preferences/syncer.h | 2 + esphome/components/rtttl/rtttl.cpp | 9 +- esphome/components/safe_mode/safe_mode.cpp | 2 + esphome/components/sntp/sntp_component.cpp | 6 + esphome/components/tlc5971/tlc5971.cpp | 5 +- esphome/core/application.cpp | 69 +++++++- esphome/core/application.h | 28 ++++ esphome/core/component.cpp | 40 +++-- esphome/core/component.h | 21 +++ tests/integration/conftest.py | 26 ++- .../loop_test_component/__init__.py | 78 +++++++++ .../loop_test_component.cpp | 43 +++++ .../loop_test_component/loop_test_component.h | 58 +++++++ .../fixtures/loop_disable_enable.yaml | 48 ++++++ tests/integration/test_loop_disable_enable.py | 150 ++++++++++++++++++ tests/integration/types.py | 14 +- 28 files changed, 646 insertions(+), 29 deletions(-) create mode 100644 tests/integration/fixtures/external_components/loop_test_component/__init__.py create mode 100644 tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp create mode 100644 tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h create mode 100644 tests/integration/fixtures/loop_disable_enable.yaml create mode 100644 tests/integration/test_loop_disable_enable.py diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index ebf6c1d037..d0e8f6827f 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -17,7 +17,11 @@ void Anova::setup() { this->current_request_ = 0; } -void Anova::loop() {} +void Anova::loop() { + // Parent BLEClientNode has a loop() method, but this component uses + // polling via update() and BLE callbacks so loop isn't needed + this->disable_loop(); +} void Anova::control(const ClimateCall &call) { if (call.get_mode().has_value()) { diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp index 7ebed2e78d..007ca1ca7d 100644 --- a/esphome/components/bedjet/bedjet_hub.cpp +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -480,7 +480,11 @@ void BedJetHub::set_clock(uint8_t hour, uint8_t minute) { /* Internal */ -void BedJetHub::loop() {} +void BedJetHub::loop() { + // Parent BLEClientNode has a loop() method, but this component uses + // polling via update() and BLE callbacks so loop isn't needed + this->disable_loop(); +} void BedJetHub::update() { this->dispatch_status_(); } void BedJetHub::dump_config() { diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index 854129f816..f22d312b5a 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -83,7 +83,11 @@ void BedJetClimate::reset_state_() { this->publish_state(); } -void BedJetClimate::loop() {} +void BedJetClimate::loop() { + // This component is controlled via the parent BedJetHub + // Empty loop not needed, disable to save CPU cycles + this->disable_loop(); +} void BedJetClimate::control(const ClimateCall &call) { ESP_LOGD(TAG, "Received BedJetClimate::control"); diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp index 81d244ce6d..663c52ac10 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp @@ -11,7 +11,11 @@ namespace ble_client { static const char *const TAG = "ble_rssi_sensor"; -void BLEClientRSSISensor::loop() {} +void BLEClientRSSISensor::loop() { + // Parent BLEClientNode has a loop() method, but this component uses + // polling via update() and BLE GAP callbacks so loop isn't needed + this->disable_loop(); +} void BLEClientRSSISensor::dump_config() { LOG_SENSOR("", "BLE Client RSSI Sensor", this); diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index f91b07fee2..d0ccfe1f2e 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -11,7 +11,11 @@ namespace ble_client { static const char *const TAG = "ble_sensor"; -void BLESensor::loop() {} +void BLESensor::loop() { + // Parent BLEClientNode has a loop() method, but this component uses + // polling via update() and BLE callbacks so loop isn't needed + this->disable_loop(); +} void BLESensor::dump_config() { LOG_SENSOR("", "BLE Sensor", this); diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp index 5083e235c6..e7da297fa0 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -14,7 +14,11 @@ static const char *const TAG = "ble_text_sensor"; static const std::string EMPTY = ""; -void BLETextSensor::loop() {} +void BLETextSensor::loop() { + // Parent BLEClientNode has a loop() method, but this component uses + // polling via update() and BLE callbacks so loop isn't needed + this->disable_loop(); +} void BLETextSensor::dump_config() { LOG_TEXT_SENSOR("", "BLE Text Sensor", this); diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 31e6c51f0f..2c1ce17fb3 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -37,7 +37,12 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { request->redirect("/?save"); } -void CaptivePortal::setup() {} +void CaptivePortal::setup() { +#ifndef USE_ARDUINO + // No DNS server needed for non-Arduino frameworks + this->disable_loop(); +#endif +} void CaptivePortal::start() { this->base_->init(); if (!this->initialized_) { @@ -50,6 +55,8 @@ void CaptivePortal::start() { this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); this->dns_server_->start(53, "*", ip); + // Re-enable loop() when DNS server is started + this->enable_loop(); #endif this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index 24d1295e6a..94db7fef50 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -21,8 +21,11 @@ class CaptivePortal : public AsyncWebHandler, public Component { void dump_config() override; #ifdef USE_ARDUINO void loop() override { - if (this->dns_server_ != nullptr) + if (this->dns_server_ != nullptr) { this->dns_server_->processNextRequest(); + } else { + this->disable_loop(); + } } #endif float get_setup_priority() const override; diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 4e61fb287c..8ae1eb1bac 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -22,6 +22,16 @@ void BLEClientBase::setup() { this->connection_index_ = connection_index++; } +void BLEClientBase::set_state(espbt::ClientState st) { + ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_.c_str(), (int) st); + ESPBTClient::set_state(st); + + if (st == espbt::ClientState::READY_TO_CONNECT) { + // Enable loop when we need to connect + this->enable_loop(); + } +} + void BLEClientBase::loop() { if (!esp32_ble::global_ble->is_active()) { this->set_state(espbt::ClientState::INIT); @@ -37,9 +47,14 @@ void BLEClientBase::loop() { } // READY_TO_CONNECT means we have discovered the device // and the scanner has been stopped by the tracker. - if (this->state_ == espbt::ClientState::READY_TO_CONNECT) { + else if (this->state_ == espbt::ClientState::READY_TO_CONNECT) { this->connect(); } + // If its idle, we can disable the loop as set_state + // will enable it again when we need to connect. + else if (this->state_ == espbt::ClientState::IDLE) { + this->disable_loop(); + } } float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 89ac04e38c..814a9664d9 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -93,6 +93,8 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { bool check_addr(esp_bd_addr_t &addr) { return memcmp(addr, this->remote_bda_, sizeof(esp_bd_addr_t)) == 0; } + void set_state(espbt::ClientState st) override; + protected: int gattc_if_; esp_bd_addr_t remote_bda_; diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 9d84d38968..d41094fda1 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -168,6 +168,8 @@ void ESP32ImprovComponent::loop() { case improv::STATE_PROVISIONED: { this->incoming_data_.clear(); this->set_status_indicator_state_(false); + // Provisioning complete, no further loop execution needed + this->disable_loop(); break; } } @@ -254,6 +256,7 @@ void ESP32ImprovComponent::start() { ESP_LOGD(TAG, "Setting Improv to start"); this->should_start_ = true; + this->enable_loop(); } void ESP32ImprovComponent::stop() { diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 8030bd0095..3f1d58fb45 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -178,18 +178,21 @@ void OnlineImage::update() { if (this->format_ == ImageFormat::BMP) { ESP_LOGD(TAG, "Allocating BMP decoder"); this->decoder_ = make_unique(this); + this->enable_loop(); } #endif // USE_ONLINE_IMAGE_BMP_SUPPORT #ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT if (this->format_ == ImageFormat::JPEG) { ESP_LOGD(TAG, "Allocating JPEG decoder"); this->decoder_ = esphome::make_unique(this); + this->enable_loop(); } #endif // USE_ONLINE_IMAGE_JPEG_SUPPORT #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT if (this->format_ == ImageFormat::PNG) { ESP_LOGD(TAG, "Allocating PNG decoder"); this->decoder_ = make_unique(this); + this->enable_loop(); } #endif // USE_ONLINE_IMAGE_PNG_SUPPORT @@ -212,6 +215,7 @@ void OnlineImage::update() { void OnlineImage::loop() { if (!this->decoder_) { // Not decoding at the moment => nothing to do. + this->disable_loop(); return; } if (!this->downloader_ || this->decoder_->is_finished()) { diff --git a/esphome/components/preferences/syncer.h b/esphome/components/preferences/syncer.h index 8976a1fe15..b6b422d4ba 100644 --- a/esphome/components/preferences/syncer.h +++ b/esphome/components/preferences/syncer.h @@ -12,6 +12,8 @@ class IntervalSyncer : public Component { void setup() override { if (this->write_interval_ != 0) { set_interval(this->write_interval_, []() { global_preferences->sync(); }); + // When using interval-based syncing, we don't need the loop + this->disable_loop(); } } void loop() override { diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index e24816fd83..2c4a0f917f 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -142,8 +142,10 @@ void Rtttl::stop() { } void Rtttl::loop() { - if (this->note_duration_ == 0 || this->state_ == State::STATE_STOPPED) + if (this->note_duration_ == 0 || this->state_ == State::STATE_STOPPED) { + this->disable_loop(); return; + } #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { @@ -391,6 +393,11 @@ void Rtttl::set_state_(State state) { this->state_ = state; ESP_LOGV(TAG, "State changed from %s to %s", LOG_STR_ARG(state_to_string(old_state)), LOG_STR_ARG(state_to_string(state))); + + // Clear loop_done when transitioning from STOPPED to any other state + if (old_state == State::STATE_STOPPED && state != State::STATE_STOPPED) { + this->enable_loop(); + } } } // namespace rtttl diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index 89c9242357..5a62604269 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -42,6 +42,8 @@ void SafeModeComponent::loop() { ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter"); this->clean_rtc(); this->boot_successful_ = true; + // Disable loop since we no longer need to check + this->disable_loop(); } } diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index f9a9981c52..c7642d0637 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -67,6 +67,12 @@ void SNTPComponent::loop() { time.minute, time.second); this->time_sync_callback_.call(); this->has_time_ = true; + +#ifdef USE_ESP_IDF + // On ESP-IDF, time sync is permanent and update() doesn't force resync + // Time is now synchronized, no need to check anymore + this->disable_loop(); +#endif } } // namespace sntp diff --git a/esphome/components/tlc5971/tlc5971.cpp b/esphome/components/tlc5971/tlc5971.cpp index ebcc3af361..05ff0a0080 100644 --- a/esphome/components/tlc5971/tlc5971.cpp +++ b/esphome/components/tlc5971/tlc5971.cpp @@ -24,8 +24,10 @@ void TLC5971::dump_config() { } void TLC5971::loop() { - if (!this->update_) + if (!this->update_) { + this->disable_loop(); return; + } uint32_t command; @@ -93,6 +95,7 @@ void TLC5971::set_channel_value(uint16_t channel, uint16_t value) { return; if (this->pwm_amounts_[channel] != value) { this->update_ = true; + this->enable_loop(); } this->pwm_amounts_[channel] = value; } diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 4ed96f7300..58df49f0f2 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -97,7 +97,13 @@ void Application::loop() { // Feed WDT with time this->feed_wdt(last_op_end_time); - for (Component *component : this->looping_components_) { + // Mark that we're in the loop for safe reentrant modifications + this->in_loop_ = true; + + for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; + this->current_loop_index_++) { + Component *component = this->looping_components_[this->current_loop_index_]; + // Update the cached time before each component runs this->loop_component_start_time_ = last_op_end_time; @@ -112,6 +118,8 @@ void Application::loop() { this->app_state_ |= new_app_state; this->feed_wdt(last_op_end_time); } + + this->in_loop_ = false; this->app_state_ = new_app_state; // Use the last component's end time instead of calling millis() again @@ -235,9 +243,66 @@ void Application::teardown_components(uint32_t timeout_ms) { } void Application::calculate_looping_components_() { + // First add all active components for (auto *obj : this->components_) { - if (obj->has_overridden_loop()) + if (obj->has_overridden_loop() && + (obj->get_component_state() & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { this->looping_components_.push_back(obj); + } + } + + this->looping_components_active_end_ = this->looping_components_.size(); + + // Then add all inactive (LOOP_DONE) components + // This handles components that called disable_loop() during setup, before this method runs + for (auto *obj : this->components_) { + if (obj->has_overridden_loop() && + (obj->get_component_state() & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { + this->looping_components_.push_back(obj); + } + } +} + +void Application::disable_component_loop_(Component *component) { + // This method must be reentrant - components can disable themselves during their own loop() call + // Linear search to find component in active section + // Most configs have 10-30 looping components (30 is on the high end) + // O(n) is acceptable here as we optimize for memory, not complexity + for (uint16_t i = 0; i < this->looping_components_active_end_; i++) { + if (this->looping_components_[i] == component) { + // Move last active component to this position + this->looping_components_active_end_--; + if (i != this->looping_components_active_end_) { + std::swap(this->looping_components_[i], this->looping_components_[this->looping_components_active_end_]); + + // If we're currently iterating and just swapped the current position + if (this->in_loop_ && i == this->current_loop_index_) { + // Decrement so we'll process the swapped component next + this->current_loop_index_--; + } + } + return; + } + } +} + +void Application::enable_component_loop_(Component *component) { + // This method must be reentrant - components can re-enable themselves during their own loop() call + // Single pass through all components to find and move if needed + // With typical 10-30 components, O(n) is faster than maintaining a map + const uint16_t size = this->looping_components_.size(); + for (uint16_t i = 0; i < size; i++) { + if (this->looping_components_[i] == component) { + if (i < this->looping_components_active_end_) { + return; // Already active + } + // Found in inactive section - move to active + if (i != this->looping_components_active_end_) { + std::swap(this->looping_components_[i], this->looping_components_[this->looping_components_active_end_]); + } + this->looping_components_active_end_++; + return; + } } } diff --git a/esphome/core/application.h b/esphome/core/application.h index f04ea05d8e..ea298638d2 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -572,13 +572,41 @@ class Application { void calculate_looping_components_(); + // These methods are called by Component::disable_loop() and Component::enable_loop() + // Components should not call these directly - use this->disable_loop() or this->enable_loop() + // to ensure component state is properly updated along with the loop partition + void disable_component_loop_(Component *component); + void enable_component_loop_(Component *component); + void feed_wdt_arch_(); /// Perform a delay while also monitoring socket file descriptors for readiness void yield_with_select_(uint32_t delay_ms); std::vector components_{}; + + // Partitioned vector design for looping components + // ================================================= + // Components are partitioned into [active | inactive] sections: + // + // looping_components_: [A, B, C, D | E, F] + // ^ + // looping_components_active_end_ (4) + // + // - Components A,B,C,D are active and will be called in loop() + // - Components E,F are inactive (disabled/failed) and won't be called + // - No flag checking needed during iteration - just loop 0 to active_end_ + // - When a component is disabled, it's swapped with the last active component + // and active_end_ is decremented + // - When a component is enabled, it's swapped with the first inactive component + // and active_end_ is incremented + // - This eliminates branch mispredictions from flag checking in the hot loop std::vector looping_components_{}; + uint16_t looping_components_active_end_{0}; + + // For safe reentrant modifications during iteration + uint16_t current_loop_index_{0}; + bool in_loop_{false}; #ifdef USE_BINARY_SENSOR std::vector binary_sensors_{}; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 0a4606074a..3117f49ac1 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -30,17 +30,18 @@ const float LATE = -100.0f; } // namespace setup_priority -// Component state uses bits 0-1 (4 states) -const uint8_t COMPONENT_STATE_MASK = 0x03; +// Component state uses bits 0-2 (8 states, 5 used) +const uint8_t COMPONENT_STATE_MASK = 0x07; const uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00; const uint8_t COMPONENT_STATE_SETUP = 0x01; const uint8_t COMPONENT_STATE_LOOP = 0x02; const uint8_t COMPONENT_STATE_FAILED = 0x03; -// Status LED uses bits 2-3 -const uint8_t STATUS_LED_MASK = 0x0C; +const uint8_t COMPONENT_STATE_LOOP_DONE = 0x04; +// Status LED uses bits 3-4 +const uint8_t STATUS_LED_MASK = 0x18; const uint8_t STATUS_LED_OK = 0x00; -const uint8_t STATUS_LED_WARNING = 0x04; // Bit 2 -const uint8_t STATUS_LED_ERROR = 0x08; // Bit 3 +const uint8_t STATUS_LED_WARNING = 0x08; // Bit 3 +const uint8_t STATUS_LED_ERROR = 0x10; // Bit 4 const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again @@ -113,6 +114,9 @@ void Component::call() { case COMPONENT_STATE_FAILED: // NOLINT(bugprone-branch-clone) // State failed: Do nothing break; + case COMPONENT_STATE_LOOP_DONE: // NOLINT(bugprone-branch-clone) + // State loop done: Do nothing, component has finished its work + break; default: break; } @@ -136,14 +140,30 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) { return false; } void Component::mark_failed() { - ESP_LOGE(TAG, "Component %s was marked as failed.", this->get_component_source()); + ESP_LOGE(TAG, "Component %s was marked as failed", this->get_component_source()); this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ |= COMPONENT_STATE_FAILED; this->status_set_error(); + // Also remove from loop since failed components shouldn't loop + App.disable_component_loop_(this); +} +void Component::disable_loop() { + ESP_LOGD(TAG, "%s loop disabled", this->get_component_source()); + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= COMPONENT_STATE_LOOP_DONE; + App.disable_component_loop_(this); +} +void Component::enable_loop() { + if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { + ESP_LOGD(TAG, "%s loop enabled", this->get_component_source()); + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= COMPONENT_STATE_LOOP; + App.enable_component_loop_(this); + } } void Component::reset_to_construction_state() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { - ESP_LOGI(TAG, "Component %s is being reset to construction state.", this->get_component_source()); + ESP_LOGI(TAG, "Component %s is being reset to construction state", this->get_component_source()); this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; // Clear error status when resetting @@ -276,8 +296,8 @@ uint32_t WarnIfComponentBlockingGuard::finish() { } if (should_warn) { const char *src = component_ == nullptr ? "" : component_->get_component_source(); - ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms).", src, blocking_time); - ESP_LOGW(TAG, "Components should block for at most 30 ms."); + ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time); + ESP_LOGW(TAG, "Components should block for at most 30 ms"); } return curr_time; diff --git a/esphome/core/component.h b/esphome/core/component.h index f77d40ae35..a37d64086a 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -58,6 +58,7 @@ extern const uint8_t COMPONENT_STATE_CONSTRUCTION; extern const uint8_t COMPONENT_STATE_SETUP; extern const uint8_t COMPONENT_STATE_LOOP; extern const uint8_t COMPONENT_STATE_FAILED; +extern const uint8_t COMPONENT_STATE_LOOP_DONE; extern const uint8_t STATUS_LED_MASK; extern const uint8_t STATUS_LED_OK; extern const uint8_t STATUS_LED_WARNING; @@ -150,6 +151,26 @@ class Component { this->mark_failed(); } + /** Disable this component's loop. The loop() method will no longer be called. + * + * This is useful for components that only need to run for a certain period of time + * or when inactive, saving CPU cycles. + * + * @note Components should call this->disable_loop() on themselves, not on other components. + * This ensures the component's state is properly updated along with the loop partition. + */ + void disable_loop(); + + /** Enable this component's loop. The loop() method will be called normally. + * + * This is useful for components that transition between active and inactive states + * and need to re-enable their loop() method when becoming active again. + * + * @note Components should call this->enable_loop() on themselves, not on other components. + * This ensures the component's state is properly updated along with the loop partition. + */ + void enable_loop(); + bool is_failed() const; bool is_ready() const; diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 90377300a6..525e3541b3 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from collections.abc import AsyncGenerator, Generator +from collections.abc import AsyncGenerator, Callable, Generator from contextlib import AbstractAsyncContextManager, asynccontextmanager import logging import os @@ -46,6 +46,7 @@ if platform.system() == "Windows": "Integration tests are not supported on Windows", allow_module_level=True ) + import pty # not available on Windows @@ -362,7 +363,10 @@ async def api_client_connected( async def _read_stream_lines( - stream: asyncio.StreamReader, lines: list[str], output_stream: TextIO + stream: asyncio.StreamReader, + lines: list[str], + output_stream: TextIO, + line_callback: Callable[[str], None] | None = None, ) -> None: """Read lines from a stream, append to list, and echo to output stream.""" log_parser = LogParser() @@ -380,6 +384,9 @@ async def _read_stream_lines( file=output_stream, flush=True, ) + # Call the callback if provided + if line_callback: + line_callback(decoded_line.rstrip()) @asynccontextmanager @@ -388,6 +395,7 @@ async def run_binary_and_wait_for_port( host: str, port: int, timeout: float = PORT_WAIT_TIMEOUT, + line_callback: Callable[[str], None] | None = None, ) -> AsyncGenerator[None]: """Run a binary, wait for it to open a port, and clean up on exit.""" # Create a pseudo-terminal to make the binary think it's running interactively @@ -435,7 +443,9 @@ async def run_binary_and_wait_for_port( # Read from output stream output_tasks = [ asyncio.create_task( - _read_stream_lines(output_reader, stdout_lines, sys.stdout) + _read_stream_lines( + output_reader, stdout_lines, sys.stdout, line_callback + ) ) ] @@ -515,6 +525,7 @@ async def run_compiled_context( compile_esphome: CompileFunction, port: int, port_socket: socket.socket | None = None, + line_callback: Callable[[str], None] | None = None, ) -> AsyncGenerator[None]: """Context manager to write, compile and run an ESPHome configuration.""" # Write the YAML config @@ -528,7 +539,9 @@ async def run_compiled_context( port_socket.close() # Run the binary and wait for the API server to start - async with run_binary_and_wait_for_port(binary_path, LOCALHOST, port): + async with run_binary_and_wait_for_port( + binary_path, LOCALHOST, port, line_callback=line_callback + ): yield @@ -542,7 +555,9 @@ async def run_compiled( port, port_socket = reserved_tcp_port def _run_compiled( - yaml_content: str, filename: str | None = None + yaml_content: str, + filename: str | None = None, + line_callback: Callable[[str], None] | None = None, ) -> AbstractAsyncContextManager[asyncio.subprocess.Process]: return run_compiled_context( yaml_content, @@ -551,6 +566,7 @@ async def run_compiled( compile_esphome, port, port_socket, + line_callback=line_callback, ) yield _run_compiled diff --git a/tests/integration/fixtures/external_components/loop_test_component/__init__.py b/tests/integration/fixtures/external_components/loop_test_component/__init__.py new file mode 100644 index 0000000000..c5eda67d1e --- /dev/null +++ b/tests/integration/fixtures/external_components/loop_test_component/__init__.py @@ -0,0 +1,78 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME + +CODEOWNERS = ["@esphome/tests"] + +loop_test_component_ns = cg.esphome_ns.namespace("loop_test_component") +LoopTestComponent = loop_test_component_ns.class_("LoopTestComponent", cg.Component) + +CONF_DISABLE_AFTER = "disable_after" +CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations" + +COMPONENT_CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(LoopTestComponent), + cv.Required(CONF_NAME): cv.string, + cv.Optional(CONF_DISABLE_AFTER, default=0): cv.int_, + cv.Optional(CONF_TEST_REDUNDANT_OPERATIONS, default=False): cv.boolean, + } +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(LoopTestComponent), + cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA), + } +).extend(cv.COMPONENT_SCHEMA) + +# Define actions +EnableAction = loop_test_component_ns.class_("EnableAction", automation.Action) +DisableAction = loop_test_component_ns.class_("DisableAction", automation.Action) + + +@automation.register_action( + "loop_test_component.enable", + EnableAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(LoopTestComponent), + } + ), +) +async def enable_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + return var + + +@automation.register_action( + "loop_test_component.disable", + DisableAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(LoopTestComponent), + } + ), +) +async def disable_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + return var + + +async def to_code(config): + # The parent config doesn't actually create a component + # We just create each sub-component + for comp_config in config[CONF_COMPONENTS]: + var = cg.new_Pvariable(comp_config[CONF_ID]) + await cg.register_component(var, comp_config) + + cg.add(var.set_name(comp_config[CONF_NAME])) + cg.add(var.set_disable_after(comp_config[CONF_DISABLE_AFTER])) + cg.add( + var.set_test_redundant_operations( + comp_config[CONF_TEST_REDUNDANT_OPERATIONS] + ) + ) diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp new file mode 100644 index 0000000000..470740c534 --- /dev/null +++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp @@ -0,0 +1,43 @@ +#include "loop_test_component.h" + +namespace esphome { +namespace loop_test_component { + +void LoopTestComponent::setup() { ESP_LOGI(TAG, "[%s] Setup called", this->name_.c_str()); } + +void LoopTestComponent::loop() { + this->loop_count_++; + ESP_LOGI(TAG, "[%s] Loop count: %d", this->name_.c_str(), this->loop_count_); + + // Test self-disable after specified count + if (this->disable_after_ > 0 && this->loop_count_ == this->disable_after_) { + ESP_LOGI(TAG, "[%s] Disabling self after %d loops", this->name_.c_str(), this->disable_after_); + this->disable_loop(); + } + + // Test redundant operations + if (this->test_redundant_operations_ && this->loop_count_ == 5) { + if (this->name_ == "redundant_enable") { + ESP_LOGI(TAG, "[%s] Testing enable when already enabled", this->name_.c_str()); + this->enable_loop(); + } else if (this->name_ == "redundant_disable") { + ESP_LOGI(TAG, "[%s] Testing disable when will be disabled", this->name_.c_str()); + // We'll disable at count 10, but try to disable again at 5 + this->disable_loop(); + ESP_LOGI(TAG, "[%s] First disable complete", this->name_.c_str()); + } + } +} + +void LoopTestComponent::service_enable() { + ESP_LOGI(TAG, "[%s] Service enable called", this->name_.c_str()); + this->enable_loop(); +} + +void LoopTestComponent::service_disable() { + ESP_LOGI(TAG, "[%s] Service disable called", this->name_.c_str()); + this->disable_loop(); +} + +} // namespace loop_test_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h new file mode 100644 index 0000000000..5c43dd4b43 --- /dev/null +++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h @@ -0,0 +1,58 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/automation.h" + +namespace esphome { +namespace loop_test_component { + +static const char *const TAG = "loop_test_component"; + +class LoopTestComponent : public Component { + public: + void set_name(const std::string &name) { this->name_ = name; } + void set_disable_after(int count) { this->disable_after_ = count; } + void set_test_redundant_operations(bool test) { this->test_redundant_operations_ = test; } + + void setup() override; + void loop() override; + + // Service methods for external control + void service_enable(); + void service_disable(); + + int get_loop_count() const { return this->loop_count_; } + + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + std::string name_; + int loop_count_{0}; + int disable_after_{0}; + bool test_redundant_operations_{false}; +}; + +template class EnableAction : public Action { + public: + EnableAction(LoopTestComponent *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->service_enable(); } + + protected: + LoopTestComponent *parent_; +}; + +template class DisableAction : public Action { + public: + DisableAction(LoopTestComponent *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->service_disable(); } + + protected: + LoopTestComponent *parent_; +}; + +} // namespace loop_test_component +} // namespace esphome diff --git a/tests/integration/fixtures/loop_disable_enable.yaml b/tests/integration/fixtures/loop_disable_enable.yaml new file mode 100644 index 0000000000..17010f7c34 --- /dev/null +++ b/tests/integration/fixtures/loop_disable_enable.yaml @@ -0,0 +1,48 @@ +esphome: + name: loop-test + +host: +api: +logger: + level: DEBUG + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +loop_test_component: + components: + # Component that disables itself after 10 loops + - id: self_disable_10 + name: "self_disable_10" + disable_after: 10 + + # Component that never disables itself (for re-enable test) + - id: normal_component + name: "normal_component" + disable_after: 0 + + # Component that tests enable when already enabled + - id: redundant_enable + name: "redundant_enable" + test_redundant_operations: true + disable_after: 0 + + # Component that tests disable when already disabled + - id: redundant_disable + name: "redundant_disable" + test_redundant_operations: true + disable_after: 10 + +# Interval to re-enable the self_disable_10 component after some time +interval: + - interval: 0.5s + then: + - if: + condition: + lambda: 'return id(self_disable_10).get_loop_count() == 10;' + then: + - logger.log: "Re-enabling self_disable_10 via service" + - loop_test_component.enable: + id: self_disable_10 diff --git a/tests/integration/test_loop_disable_enable.py b/tests/integration/test_loop_disable_enable.py new file mode 100644 index 0000000000..84301c25d8 --- /dev/null +++ b/tests/integration/test_loop_disable_enable.py @@ -0,0 +1,150 @@ +"""Integration test for loop disable/enable functionality.""" + +from __future__ import annotations + +import asyncio +from pathlib import Path +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_loop_disable_enable( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that components can disable and enable their loop() method.""" + # Get the absolute path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + # Track log messages and events + log_messages: list[str] = [] + + # Event fired when self_disable_10 component disables itself after 10 loops + self_disable_10_disabled = asyncio.Event() + # Event fired when normal_component reaches 10 loops + normal_component_10_loops = asyncio.Event() + # Event fired when redundant_enable component tests enabling when already enabled + redundant_enable_tested = asyncio.Event() + # Event fired when redundant_disable component tests disabling when already disabled + redundant_disable_tested = asyncio.Event() + # Event fired when self_disable_10 component is re-enabled and runs again (count > 10) + self_disable_10_re_enabled = asyncio.Event() + + # Track loop counts for components + self_disable_10_counts: list[int] = [] + normal_component_counts: list[int] = [] + + def on_log_line(line: str) -> None: + """Process each log line from the process output.""" + # Strip ANSI color codes + clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) + + if "loop_test_component" not in clean_line: + return + + log_messages.append(clean_line) + + # Track specific events using the cleaned line + if "[self_disable_10]" in clean_line: + if "Loop count:" in clean_line: + # Extract loop count + try: + count = int(clean_line.split("Loop count: ")[1]) + self_disable_10_counts.append(count) + # Check if component was re-enabled (count > 10) + if count > 10: + self_disable_10_re_enabled.set() + except (IndexError, ValueError): + pass + elif "Disabling self after 10 loops" in clean_line: + self_disable_10_disabled.set() + + elif "[normal_component]" in clean_line and "Loop count:" in clean_line: + try: + count = int(clean_line.split("Loop count: ")[1]) + normal_component_counts.append(count) + if count >= 10: + normal_component_10_loops.set() + except (IndexError, ValueError): + pass + + elif ( + "[redundant_enable]" in clean_line + and "Testing enable when already enabled" in clean_line + ): + redundant_enable_tested.set() + + elif ( + "[redundant_disable]" in clean_line + and "Testing disable when will be disabled" in clean_line + ): + redundant_disable_tested.set() + + # Write, compile and run the ESPHome device with log callback + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect and get device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "loop-test" + + # Wait for self_disable_10 to disable itself + try: + await asyncio.wait_for(self_disable_10_disabled.wait(), timeout=10.0) + except asyncio.TimeoutError: + pytest.fail("self_disable_10 did not disable itself within 10 seconds") + + # Verify it ran at least 10 times before disabling + assert len([c for c in self_disable_10_counts if c <= 10]) == 10, ( + f"Expected exactly 10 loops before disable, got {[c for c in self_disable_10_counts if c <= 10]}" + ) + assert self_disable_10_counts[:10] == list(range(1, 11)), ( + f"Expected first 10 counts to be 1-10, got {self_disable_10_counts[:10]}" + ) + + # Wait for normal_component to run at least 10 times + try: + await asyncio.wait_for(normal_component_10_loops.wait(), timeout=10.0) + except asyncio.TimeoutError: + pytest.fail( + f"normal_component did not reach 10 loops within timeout, got {len(normal_component_counts)}" + ) + + # Wait for redundant operation tests + try: + await asyncio.wait_for(redundant_enable_tested.wait(), timeout=10.0) + except asyncio.TimeoutError: + pytest.fail("redundant_enable did not test enabling when already enabled") + + try: + await asyncio.wait_for(redundant_disable_tested.wait(), timeout=10.0) + except asyncio.TimeoutError: + pytest.fail( + "redundant_disable did not test disabling when will be disabled" + ) + + # Wait to see if self_disable_10 gets re-enabled + try: + await asyncio.wait_for(self_disable_10_re_enabled.wait(), timeout=5.0) + except asyncio.TimeoutError: + pytest.fail("self_disable_10 was not re-enabled within 5 seconds") + + # Component was re-enabled - verify it ran more times + later_self_disable_counts = [c for c in self_disable_10_counts if c > 10] + assert later_self_disable_counts, ( + "self_disable_10 was re-enabled but did not run additional times" + ) diff --git a/tests/integration/types.py b/tests/integration/types.py index 6fc3e9435e..5e4bfaa29d 100644 --- a/tests/integration/types.py +++ b/tests/integration/types.py @@ -13,7 +13,19 @@ from aioesphomeapi import APIClient ConfigWriter = Callable[[str, str | None], Awaitable[Path]] CompileFunction = Callable[[Path], Awaitable[Path]] RunFunction = Callable[[Path], Awaitable[asyncio.subprocess.Process]] -RunCompiledFunction = Callable[[str, str | None], AbstractAsyncContextManager[None]] + + +class RunCompiledFunction(Protocol): + """Protocol for run_compiled function with optional line callback.""" + + def __call__( # noqa: E704 + self, + yaml_content: str, + filename: str | None = None, + line_callback: Callable[[str], None] | None = None, + ) -> AbstractAsyncContextManager[None]: ... + + WaitFunction = Callable[[APIClient, float], Awaitable[bool]] From c612985930be8a70f7fd1abc24070740cd147448 Mon Sep 17 00:00:00 2001 From: Severin von Wnuck-Lipinski Date: Wed, 18 Jun 2025 11:49:39 +0200 Subject: [PATCH 053/293] Add support for Xiaomi XMWSDJ04MMC (#8591) --- CODEOWNERS | 1 + esphome/components/xiaomi_ble/xiaomi_ble.cpp | 12 +++ esphome/components/xiaomi_ble/xiaomi_ble.h | 1 + .../components/xiaomi_xmwsdj04mmc/__init__.py | 0 .../components/xiaomi_xmwsdj04mmc/sensor.py | 77 +++++++++++++++++++ .../xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp | 77 +++++++++++++++++++ .../xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h | 37 +++++++++ .../components/xiaomi_xmwsdj04mmc/common.yaml | 12 +++ .../xiaomi_xmwsdj04mmc/test.esp32-ard.yaml | 1 + .../xiaomi_xmwsdj04mmc/test.esp32-c3-ard.yaml | 1 + .../xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml | 1 + .../xiaomi_xmwsdj04mmc/test.esp32-idf.yaml | 1 + 12 files changed, 221 insertions(+) create mode 100644 esphome/components/xiaomi_xmwsdj04mmc/__init__.py create mode 100644 esphome/components/xiaomi_xmwsdj04mmc/sensor.py create mode 100644 esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp create mode 100644 esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h create mode 100644 tests/components/xiaomi_xmwsdj04mmc/common.yaml create mode 100644 tests/components/xiaomi_xmwsdj04mmc/test.esp32-ard.yaml create mode 100644 tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-ard.yaml create mode 100644 tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml create mode 100644 tests/components/xiaomi_xmwsdj04mmc/test.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 66ea80f8d6..ebbc8732ea 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -520,6 +520,7 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs esphome/components/xiaomi_rtcgq02lm/* @jesserockz +esphome/components/xiaomi_xmwsdj04mmc/* @medusalix esphome/components/xl9535/* @mreditor97 esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/xxtea/* @clydebarrow diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 04e0724ba7..a80daa0b80 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -91,6 +91,13 @@ bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_ // MiaoMiaoce humidity, 1 byte, 8-bit unsigned integer, 1 % else if ((value_type == 0x4C02) && (value_length == 1)) { result.humidity = data[0]; + } + // XMWSDJ04MMC humidity, 4 bytes, float, 0.1 °C + else if ((value_type == 0x4C08) && (value_length == 4)) { + const uint32_t int_number = encode_uint32(data[3], data[2], data[1], data[0]); + float humidity; + std::memcpy(&humidity, &int_number, sizeof(humidity)); + result.humidity = humidity; } else { return false; } @@ -219,6 +226,11 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service } else if (device_uuid == 0x055b) { // small square body, segment LCD, encrypted result.type = XiaomiParseResult::TYPE_LYWSD03MMC; result.name = "LYWSD03MMC"; + } else if (device_uuid == 0x1203) { // small square body, e-ink display, encrypted + result.type = XiaomiParseResult::TYPE_XMWSDJ04MMC; + result.name = "XMWSDJ04MMC"; + if (raw.size() == 19) + result.raw_offset -= 6; } else if (device_uuid == 0x07f6) { // Xiaomi-Yeelight BLE nightlight result.type = XiaomiParseResult::TYPE_MJYD02YLA; result.name = "MJYD02YLA"; diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index 6978be97f4..77fb04fd78 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -20,6 +20,7 @@ struct XiaomiParseResult { TYPE_LYWSD02MMC, TYPE_CGG1, TYPE_LYWSD03MMC, + TYPE_XMWSDJ04MMC, TYPE_CGD1, TYPE_CGDK2, TYPE_JQJCY01YM, diff --git a/esphome/components/xiaomi_xmwsdj04mmc/__init__.py b/esphome/components/xiaomi_xmwsdj04mmc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_xmwsdj04mmc/sensor.py b/esphome/components/xiaomi_xmwsdj04mmc/sensor.py new file mode 100644 index 0000000000..b41a775f35 --- /dev/null +++ b/esphome/components/xiaomi_xmwsdj04mmc/sensor.py @@ -0,0 +1,77 @@ +import esphome.codegen as cg +from esphome.components import esp32_ble_tracker, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_BINDKEY, + CONF_HUMIDITY, + CONF_ID, + CONF_MAC_ADDRESS, + CONF_TEMPERATURE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +AUTO_LOAD = ["xiaomi_ble"] +CODEOWNERS = ["@medusalix"] +DEPENDENCIES = ["esp32_ble_tracker"] + +xiaomi_xmwsdj04mmc_ns = cg.esphome_ns.namespace("xiaomi_xmwsdj04mmc") +XiaomiXMWSDJ04MMC = xiaomi_xmwsdj04mmc_ns.class_( + "XiaomiXMWSDJ04MMC", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(XiaomiXMWSDJ04MMC), + cv.Required(CONF_BINDKEY): cv.bind_key, + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature(sens)) + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity(sens)) + if battery_level_config := config.get(CONF_BATTERY_LEVEL): + sens = await sensor.new_sensor(battery_level_config) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp new file mode 100644 index 0000000000..f8712e7fd4 --- /dev/null +++ b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp @@ -0,0 +1,77 @@ +#include "xiaomi_xmwsdj04mmc.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_xmwsdj04mmc { + +static const char *const TAG = "xiaomi_xmwsdj04mmc"; + +void XiaomiXMWSDJ04MMC::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi XMWSDJ04MMC"); + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiXMWSDJ04MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (res->humidity.has_value() && this->humidity_ != nullptr) { + // see https://github.com/custom-components/sensor.mitemp_bt/issues/7#issuecomment-595948254 + *res->humidity = trunc(*res->humidity); + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + return success; +} + +void XiaomiXMWSDJ04MMC::set_bindkey(const std::string &bindkey) { + memset(this->bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + this->bindkey_[i] = std::strtoul(temp, nullptr, 16); + } +} + +} // namespace xiaomi_xmwsdj04mmc +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h new file mode 100644 index 0000000000..9ce02bb64e --- /dev/null +++ b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_xmwsdj04mmc { + +class XiaomiXMWSDJ04MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { this->address_ = address; } + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; } + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_xmwsdj04mmc +} // namespace esphome + +#endif diff --git a/tests/components/xiaomi_xmwsdj04mmc/common.yaml b/tests/components/xiaomi_xmwsdj04mmc/common.yaml new file mode 100644 index 0000000000..fe7a11efc5 --- /dev/null +++ b/tests/components/xiaomi_xmwsdj04mmc/common.yaml @@ -0,0 +1,12 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_xmwsdj04mmc + mac_address: 84:B4:DB:5D:A3:8F + bindkey: d8ca2ed09bb5541dc8f045ca360b00ea + temperature: + name: Xiaomi XMWSDJ04MMC Temperature + humidity: + name: Xiaomi XMWSDJ04MMC Humidity + battery_level: + name: Xiaomi XMWSDJ04MMC Battery Level diff --git a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-ard.yaml b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-ard.yaml b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-idf.yaml b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 669ef7a0b132df86ec392bc22790e6a7c24c7cf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 18 Jun 2025 11:51:00 +0200 Subject: [PATCH 054/293] [web_server] Upgrade ESPAsync libraries (#8867) --- esphome/components/async_tcp/__init__.py | 8 ++-- .../captive_portal/captive_portal.cpp | 4 ++ .../captive_portal/captive_portal.h | 2 +- .../prometheus/prometheus_handler.h | 2 +- esphome/components/web_server/web_server.cpp | 37 +++++++++++++------ esphome/components/web_server/web_server.h | 6 +-- .../components/web_server_base/__init__.py | 6 ++- .../web_server_base/web_server_base.h | 8 ++-- .../web_server_idf/web_server_idf.h | 10 ++--- platformio.ini | 6 +-- 10 files changed, 54 insertions(+), 35 deletions(-) diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index 99e250b6fc..eec6a0e327 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -21,8 +21,8 @@ CONFIG_SCHEMA = cv.All( @coroutine_with_priority(200.0) async def to_code(config): if CORE.is_esp32 or CORE.is_libretiny: - # https://github.com/esphome/AsyncTCP/blob/master/library.json - cg.add_library("esphome/AsyncTCP-esphome", "2.1.4") + # https://github.com/ESP32Async/AsyncTCP + cg.add_library("ESP32Async/AsyncTCP", "3.4.4") elif CORE.is_esp8266: - # https://github.com/esphome/ESPAsyncTCP - cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0") + # https://github.com/ESP32Async/ESPAsyncTCP + cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0") diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 2c1ce17fb3..51e5cfc8ff 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -75,7 +75,11 @@ void CaptivePortal::start() { void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { if (req->url() == "/") { +#ifndef USE_ESP8266 + auto *response = req->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); +#else auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); +#endif response->addHeader("Content-Encoding", "gzip"); req->send(response); return; diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index 94db7fef50..c78fff824a 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -40,7 +40,7 @@ class CaptivePortal : public AsyncWebHandler, public Component { #endif } - bool canHandle(AsyncWebServerRequest *request) override { + bool canHandle(AsyncWebServerRequest *request) const override { if (!this->active_) return false; diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index bdc3d971ce..c4598f44b0 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -40,7 +40,7 @@ class PrometheusHandler : public AsyncWebHandler, public Component { */ void add_label_name(EntityBase *obj, const std::string &value) { relabel_map_name_.insert({obj, value}); } - bool canHandle(AsyncWebServerRequest *request) override { + bool canHandle(AsyncWebServerRequest *request) const override { if (request->method() == HTTP_GET) { if (request->url() == "/metrics") return true; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 7ae30522f4..becb5bc2c7 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -91,7 +91,7 @@ void DeferredUpdateEventSource::process_deferred_queue_() { while (!deferred_queue_.empty()) { DeferredEvent &de = deferred_queue_.front(); std::string message = de.message_generator_(web_server_, de.source_); - if (this->try_send(message.c_str(), "state")) { + if (this->send(message.c_str(), "state") != DISCARDED) { // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen deferred_queue_.erase(deferred_queue_.begin()); } else { @@ -131,7 +131,7 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char * deq_push_back_with_dedup_(source, message_generator); } else { std::string message = message_generator(web_server_, source); - if (!this->try_send(message.c_str(), "state")) { + if (this->send(message.c_str(), "state") == DISCARDED) { deq_push_back_with_dedup_(source, message_generator); } } @@ -171,8 +171,8 @@ void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServer ws->defer([this, ws, es]() { this->on_client_connect_(ws, es); }); }); - es->onDisconnect([this, ws](AsyncEventSource *source, AsyncEventSourceClient *client) { - ws->defer([this, source]() { this->on_client_disconnect_((DeferredUpdateEventSource *) source); }); + es->onDisconnect([this, ws, es](AsyncEventSourceClient *client) { + ws->defer([this, es]() { this->on_client_disconnect_((DeferredUpdateEventSource *) es); }); }); es->handleRequest(request); @@ -291,14 +291,23 @@ float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f #ifdef USE_WEBSERVER_LOCAL void WebServer::handle_index_request(AsyncWebServerRequest *request) { +#ifndef USE_ESP8266 + AsyncWebServerResponse *response = request->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); +#else AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); +#endif response->addHeader("Content-Encoding", "gzip"); request->send(response); } #elif USE_WEBSERVER_VERSION >= 2 void WebServer::handle_index_request(AsyncWebServerRequest *request) { +#ifndef USE_ESP8266 + AsyncWebServerResponse *response = + request->beginResponse(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE); +#else AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE); +#endif // No gzip header here because the HTML file is so small request->send(response); } @@ -317,8 +326,13 @@ void WebServer::handle_pna_cors_request(AsyncWebServerRequest *request) { #ifdef USE_WEBSERVER_CSS_INCLUDE void WebServer::handle_css_request(AsyncWebServerRequest *request) { +#ifndef USE_ESP8266 + AsyncWebServerResponse *response = + request->beginResponse(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE); +#else AsyncWebServerResponse *response = request->beginResponse_P(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE); +#endif response->addHeader("Content-Encoding", "gzip"); request->send(response); } @@ -326,8 +340,13 @@ void WebServer::handle_css_request(AsyncWebServerRequest *request) { #ifdef USE_WEBSERVER_JS_INCLUDE void WebServer::handle_js_request(AsyncWebServerRequest *request) { +#ifndef USE_ESP8266 + AsyncWebServerResponse *response = + request->beginResponse(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE); +#else AsyncWebServerResponse *response = request->beginResponse_P(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE); +#endif response->addHeader("Content-Encoding", "gzip"); request->send(response); } @@ -1837,7 +1856,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c } #endif -bool WebServer::canHandle(AsyncWebServerRequest *request) { +bool WebServer::canHandle(AsyncWebServerRequest *request) const { if (request->url() == "/") return true; @@ -1859,12 +1878,6 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS if (request->method() == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) { -#ifdef USE_ARDUINO - // Header needs to be added to interesting header list for it to not be - // nuked by the time we handle the request later. - // Only required in Arduino framework. - request->addInterestingHeader(HEADER_CORS_REQ_PNA); -#endif return true; } #endif @@ -2145,7 +2158,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { #endif } -bool WebServer::isRequestHandlerTrivial() { return false; } +bool WebServer::isRequestHandlerTrivial() const { return false; } void WebServer::add_entity_config(EntityBase *entity, float weight, uint64_t group) { this->sorting_entitys_[entity] = SortingComponents{weight, group}; diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index f4d6ad8e86..53ee4d1212 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -99,7 +99,7 @@ class DeferredUpdateEventSource : public AsyncEventSource { protected: // surface a couple methods from the base class using AsyncEventSource::handleRequest; - using AsyncEventSource::try_send; + using AsyncEventSource::send; ListEntitiesIterator entities_iterator_; // vector is used very specifically for its zero memory overhead even though items are popped from the front (memory @@ -468,11 +468,11 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif /// Override the web handler's canHandle method. - bool canHandle(AsyncWebServerRequest *request) override; + bool canHandle(AsyncWebServerRequest *request) const override; /// Override the web handler's handleRequest method. void handleRequest(AsyncWebServerRequest *request) override; /// This web handle is not trivial. - bool isRequestHandlerTrivial() override; // NOLINT(readability-identifier-naming) + bool isRequestHandlerTrivial() const override; // NOLINT(readability-identifier-naming) void add_entity_config(EntityBase *entity, float weight, uint64_t group); void add_sorting_group(uint64_t group_id, const std::string &group_name, float weight); diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index f50ee59b9c..c17bab2128 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -36,5 +36,7 @@ async def to_code(config): cg.add_library("WiFi", None) cg.add_library("FS", None) cg.add_library("Update", None) - # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json - cg.add_library("esphome/ESPAsyncWebServer-esphome", "3.3.0") + if CORE.is_esp8266: + cg.add_library("ESP8266WiFi", None) + # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json + cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8") diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index f876d163bc..641006cb99 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -23,7 +23,7 @@ class MiddlewareHandler : public AsyncWebHandler { public: MiddlewareHandler(AsyncWebHandler *next) : next_(next) {} - bool canHandle(AsyncWebServerRequest *request) override { return next_->canHandle(request); } + bool canHandle(AsyncWebServerRequest *request) const override { return next_->canHandle(request); } void handleRequest(AsyncWebServerRequest *request) override { next_->handleRequest(request); } void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) override { @@ -32,7 +32,7 @@ class MiddlewareHandler : public AsyncWebHandler { void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override { next_->handleBody(request, data, len, index, total); } - bool isRequestHandlerTrivial() override { return next_->isRequestHandlerTrivial(); } + bool isRequestHandlerTrivial() const override { return next_->isRequestHandlerTrivial(); } protected: AsyncWebHandler *next_; @@ -131,12 +131,12 @@ class OTARequestHandler : public AsyncWebHandler { void handleRequest(AsyncWebServerRequest *request) override; void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) override; - bool canHandle(AsyncWebServerRequest *request) override { + bool canHandle(AsyncWebServerRequest *request) const override { return request->url() == "/update" && request->method() == HTTP_POST; } // NOLINTNEXTLINE(readability-identifier-naming) - bool isRequestHandlerTrivial() override { return false; } + bool isRequestHandlerTrivial() const override { return false; } protected: uint32_t last_ota_progress_{0}; diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index d883c0ca9b..8dafdf11ef 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -135,8 +135,8 @@ class AsyncWebServerRequest { return res; } // NOLINTNEXTLINE(readability-identifier-naming) - AsyncWebServerResponse *beginResponse_P(int code, const char *content_type, const uint8_t *data, - const size_t data_size) { + AsyncWebServerResponse *beginResponse(int code, const char *content_type, const uint8_t *data, + const size_t data_size) { auto *res = new AsyncWebServerResponseProgmem(this, data, data_size); // NOLINT(cppcoreguidelines-owning-memory) this->init_response_(res, code, content_type); return res; @@ -211,7 +211,7 @@ class AsyncWebHandler { public: virtual ~AsyncWebHandler() {} // NOLINTNEXTLINE(readability-identifier-naming) - virtual bool canHandle(AsyncWebServerRequest *request) { return false; } + virtual bool canHandle(AsyncWebServerRequest *request) const { return false; } // NOLINTNEXTLINE(readability-identifier-naming) virtual void handleRequest(AsyncWebServerRequest *request) {} // NOLINTNEXTLINE(readability-identifier-naming) @@ -220,7 +220,7 @@ class AsyncWebHandler { // NOLINTNEXTLINE(readability-identifier-naming) virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {} // NOLINTNEXTLINE(readability-identifier-naming) - virtual bool isRequestHandlerTrivial() { return true; } + virtual bool isRequestHandlerTrivial() const { return true; } }; #ifdef USE_WEBSERVER @@ -290,7 +290,7 @@ class AsyncEventSource : public AsyncWebHandler { ~AsyncEventSource() override; // NOLINTNEXTLINE(readability-identifier-naming) - bool canHandle(AsyncWebServerRequest *request) override { + bool canHandle(AsyncWebServerRequest *request) const override { return request->method() == HTTP_GET && request->url() == this->url_; } // NOLINTNEXTLINE(readability-identifier-naming) diff --git a/platformio.ini b/platformio.ini index 58e0b77c07..96926eadd1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -65,7 +65,7 @@ lib_deps = SPI ; spi (Arduino built-in) Wire ; i2c (Arduino built-int) heman/AsyncMqttClient-esphome@1.0.0 ; mqtt - esphome/ESPAsyncWebServer-esphome@3.3.0 ; web_server_base + ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base fastled/FastLED@3.9.16 ; fastled_base mikalhart/TinyGPSPlus@1.1.0 ; gps freekode/TM1651@1.0.1 ; tm1651 @@ -100,7 +100,7 @@ lib_deps = ${common:arduino.lib_deps} ESP8266WiFi ; wifi (Arduino built-in) Update ; ota (Arduino built-in) - esphome/ESPAsyncTCP-esphome@2.0.0 ; async_tcp + ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) @@ -130,7 +130,7 @@ lib_deps = WiFi ; wifi,web_server_base,ethernet (Arduino built-in) Update ; ota,web_server_base (Arduino built-in) ${common:arduino.lib_deps} - esphome/AsyncTCP-esphome@2.1.4 ; async_tcp + ESP32Async/AsyncTCP@3.4.4 ; async_tcp WiFiClientSecure ; http_request,nextion (Arduino built-in) HTTPClient ; http_request,nextion (Arduino built-in) ESPmDNS ; mdns (Arduino built-in) From 6667336bd89786f5546dd07dc120d6642beabb39 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Jun 2025 11:57:49 +0200 Subject: [PATCH 055/293] Eliminate memory fragmentation with BLE event pool (#9101) --- esphome/components/esp32_ble/ble.cpp | 55 ++-- esphome/components/esp32_ble/ble.h | 2 + esphome/components/esp32_ble/ble_event.h | 275 +++++++++++------- esphome/components/esp32_ble/ble_event_pool.h | 72 +++++ esphome/components/esp32_ble/queue.h | 25 +- 5 files changed, 288 insertions(+), 141 deletions(-) create mode 100644 esphome/components/esp32_ble/ble_event_pool.h diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 8adef79d2f..5a66f11d0f 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP32 #include "ble.h" +#include "ble_event_pool.h" #include "esphome/core/application.h" #include "esphome/core/log.h" @@ -23,9 +24,6 @@ namespace esp32_ble { static const char *const TAG = "esp32_ble"; -static RAMAllocator EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - RAMAllocator::ALLOW_FAILURE | RAMAllocator::ALLOC_INTERNAL); - void ESP32BLE::setup() { global_ble = this; ESP_LOGCONFIG(TAG, "Running setup"); @@ -349,9 +347,8 @@ void ESP32BLE::loop() { default: break; } - // Destructor will clean up external allocations for GATTC/GATTS - ble_event->~BLEEvent(); - EVENT_ALLOCATOR.deallocate(ble_event, 1); + // Return the event to the pool + this->ble_event_pool_.release(ble_event); ble_event = this->ble_events_.pop(); } if (this->advertising_ != nullptr) { @@ -359,37 +356,41 @@ void ESP32BLE::loop() { } // Log dropped events periodically - size_t dropped = this->ble_events_.get_and_reset_dropped_count(); + uint16_t dropped = this->ble_events_.get_and_reset_dropped_count(); if (dropped > 0) { - ESP_LOGW(TAG, "Dropped %zu BLE events due to buffer overflow", dropped); + ESP_LOGW(TAG, "Dropped %u BLE events due to buffer overflow", dropped); } } +// Helper function to load new event data based on type +void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { + event->load_gap_event(e, p); +} + +void load_ble_event(BLEEvent *event, esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + event->load_gattc_event(e, i, p); +} + +void load_ble_event(BLEEvent *event, esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { + event->load_gatts_event(e, i, p); +} + template void enqueue_ble_event(Args... args) { - // Check if queue is full before allocating - if (global_ble->ble_events_.full()) { - // Queue is full, drop the event + // Allocate an event from the pool + BLEEvent *event = global_ble->ble_event_pool_.allocate(); + if (event == nullptr) { + // No events available - queue is full or we're out of memory global_ble->ble_events_.increment_dropped_count(); return; } - BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); - if (new_event == nullptr) { - // Memory too fragmented to allocate new event. Can only drop it until memory comes back - global_ble->ble_events_.increment_dropped_count(); - return; - } - new (new_event) BLEEvent(args...); + // Load new event data (replaces previous event) + load_ble_event(event, args...); - // Push the event - since we're the only producer and we checked full() above, - // this should always succeed unless we have a bug - if (!global_ble->ble_events_.push(new_event)) { - // This should not happen in SPSC queue with single producer - ESP_LOGE(TAG, "BLE queue push failed unexpectedly"); - new_event->~BLEEvent(); - EVENT_ALLOCATOR.deallocate(new_event, 1); - } -} // NOLINT(clang-analyzer-unix.Malloc) + // Push the event to the queue + global_ble->ble_events_.push(event); + // Push always succeeds because we're the only producer and the pool ensures we never exceed queue size +} // Explicit template instantiations for the friend function template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *); diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 58c064a2ef..9fe996086e 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -12,6 +12,7 @@ #include "esphome/core/helpers.h" #include "ble_event.h" +#include "ble_event_pool.h" #include "queue.h" #ifdef USE_ESP32 @@ -148,6 +149,7 @@ class ESP32BLE : public Component { BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; LockFreeQueue ble_events_; + BLEEventPool ble_event_pool_; BLEAdvertising *advertising_{}; esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; uint32_t advertising_cycle_time_{}; diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index f51095effd..30118d2afd 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -51,6 +51,13 @@ static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) == // - GATTC/GATTS events: We heap-allocate and copy the entire param struct, ensuring // the data remains valid even after the BLE callback returns. The original // param pointer from ESP-IDF is only valid during the callback. +// +// CRITICAL DESIGN NOTE: +// The heap allocations for GATTC/GATTS events are REQUIRED for memory safety. +// DO NOT attempt to optimize by removing these allocations or storing pointers +// to the original ESP-IDF data. The ESP-IDF callback data has a different lifetime +// than our event processing, and accessing it after the callback returns would +// result in use-after-free bugs and crashes. class BLEEvent { public: // NOLINTNEXTLINE(readability-identifier-naming) @@ -63,123 +70,72 @@ class BLEEvent { // Constructor for GAP events - no external allocations needed BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { this->type_ = GAP; - this->event_.gap.gap_event = e; - - if (p == nullptr) { - return; // Invalid event, but we can't log in header file - } - - // Only copy the data we actually use for each GAP event type - switch (e) { - case ESP_GAP_BLE_SCAN_RESULT_EVT: - // Copy only the fields we use from scan results - memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t)); - this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type; - this->event_.gap.scan_result.rssi = p->scan_rst.rssi; - this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len; - this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len; - this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt; - memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv, - ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX); - break; - - case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: - this->event_.gap.scan_complete.status = p->scan_param_cmpl.status; - break; - - case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: - this->event_.gap.scan_complete.status = p->scan_start_cmpl.status; - break; - - case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: - this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status; - break; - - default: - // We only handle 4 GAP event types, others are dropped - break; - } + this->init_gap_data_(e, p); } // Constructor for GATTC events - uses heap allocation - // Creates a copy of the param struct since the original is only valid during the callback + // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization. + // The param pointer from ESP-IDF is only valid during the callback execution. + // Since BLE events are processed asynchronously in the main loop, we must create + // our own copy to ensure the data remains valid until the event is processed. BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { this->type_ = GATTC; - this->event_.gattc.gattc_event = e; - this->event_.gattc.gattc_if = i; - - if (p == nullptr) { - this->event_.gattc.gattc_param = nullptr; - this->event_.gattc.data = nullptr; - return; // Invalid event, but we can't log in header file - } - - // Heap-allocate param and data - // Heap allocation is used because GATTC/GATTS events are rare (<1% of events) - // while GAP events (99%) are stored inline to minimize memory usage - this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p); - - // Copy data for events that need it - switch (e) { - case ESP_GATTC_NOTIFY_EVT: - this->event_.gattc.data = new std::vector(p->notify.value, p->notify.value + p->notify.value_len); - this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data(); - break; - case ESP_GATTC_READ_CHAR_EVT: - case ESP_GATTC_READ_DESCR_EVT: - this->event_.gattc.data = new std::vector(p->read.value, p->read.value + p->read.value_len); - this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data(); - break; - default: - this->event_.gattc.data = nullptr; - break; - } + this->init_gattc_data_(e, i, p); } // Constructor for GATTS events - uses heap allocation - // Creates a copy of the param struct since the original is only valid during the callback + // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization. + // The param pointer from ESP-IDF is only valid during the callback execution. + // Since BLE events are processed asynchronously in the main loop, we must create + // our own copy to ensure the data remains valid until the event is processed. BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { this->type_ = GATTS; - this->event_.gatts.gatts_event = e; - this->event_.gatts.gatts_if = i; - - if (p == nullptr) { - this->event_.gatts.gatts_param = nullptr; - this->event_.gatts.data = nullptr; - return; // Invalid event, but we can't log in header file - } - - // Heap-allocate param and data - // Heap allocation is used because GATTC/GATTS events are rare (<1% of events) - // while GAP events (99%) are stored inline to minimize memory usage - this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p); - - // Copy data for events that need it - switch (e) { - case ESP_GATTS_WRITE_EVT: - this->event_.gatts.data = new std::vector(p->write.value, p->write.value + p->write.len); - this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data(); - break; - default: - this->event_.gatts.data = nullptr; - break; - } + this->init_gatts_data_(e, i, p); } // Destructor to clean up heap allocations - ~BLEEvent() { - switch (this->type_) { - case GATTC: - delete this->event_.gattc.gattc_param; - delete this->event_.gattc.data; - break; - case GATTS: - delete this->event_.gatts.gatts_param; - delete this->event_.gatts.data; - break; - default: - break; + ~BLEEvent() { this->cleanup_heap_data(); } + + // Default constructor for pre-allocation in pool + BLEEvent() : type_(GAP) {} + + // Clean up any heap-allocated data + void cleanup_heap_data() { + if (this->type_ == GAP) { + return; } + if (this->type_ == GATTC) { + delete this->event_.gattc.gattc_param; + delete this->event_.gattc.data; + this->event_.gattc.gattc_param = nullptr; + this->event_.gattc.data = nullptr; + return; + } + if (this->type_ == GATTS) { + delete this->event_.gatts.gatts_param; + delete this->event_.gatts.data; + this->event_.gatts.gatts_param = nullptr; + this->event_.gatts.data = nullptr; + } + } + + // Load new event data for reuse (replaces previous event data) + void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { + this->cleanup_heap_data(); + this->type_ = GAP; + this->init_gap_data_(e, p); + } + + void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + this->cleanup_heap_data(); + this->type_ = GATTC; + this->init_gattc_data_(e, i, p); + } + + void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { + this->cleanup_heap_data(); + this->type_ = GATTS; + this->init_gatts_data_(e, i, p); } // Disable copy to prevent double-delete @@ -224,6 +180,119 @@ class BLEEvent { esp_gap_ble_cb_event_t gap_event_type() const { return event_.gap.gap_event; } const BLEScanResult &scan_result() const { return event_.gap.scan_result; } esp_bt_status_t scan_complete_status() const { return event_.gap.scan_complete.status; } + + private: + // Initialize GAP event data + void init_gap_data_(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { + this->event_.gap.gap_event = e; + + if (p == nullptr) { + return; // Invalid event, but we can't log in header file + } + + // Copy data based on event type + switch (e) { + case ESP_GAP_BLE_SCAN_RESULT_EVT: + memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t)); + this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type; + this->event_.gap.scan_result.rssi = p->scan_rst.rssi; + this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len; + this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len; + this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt; + memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv, + ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX); + break; + + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + this->event_.gap.scan_complete.status = p->scan_param_cmpl.status; + break; + + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + this->event_.gap.scan_complete.status = p->scan_start_cmpl.status; + break; + + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status; + break; + + default: + // We only handle 4 GAP event types, others are dropped + break; + } + } + + // Initialize GATTC event data + void init_gattc_data_(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + this->event_.gattc.gattc_event = e; + this->event_.gattc.gattc_if = i; + + if (p == nullptr) { + this->event_.gattc.gattc_param = nullptr; + this->event_.gattc.data = nullptr; + return; // Invalid event, but we can't log in header file + } + + // Heap-allocate param and data + // Heap allocation is used because GATTC/GATTS events are rare (<1% of events) + // while GAP events (99%) are stored inline to minimize memory usage + // IMPORTANT: This heap allocation provides clear ownership semantics: + // - The BLEEvent owns the allocated memory for its lifetime + // - The data remains valid from the BLE callback context until processed in the main loop + // - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory + this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p); + + // Copy data for events that need it + // The param struct contains pointers (e.g., notify.value) that point to temporary buffers. + // We must copy this data to ensure it remains valid when the event is processed later. + switch (e) { + case ESP_GATTC_NOTIFY_EVT: + this->event_.gattc.data = new std::vector(p->notify.value, p->notify.value + p->notify.value_len); + this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data(); + break; + case ESP_GATTC_READ_CHAR_EVT: + case ESP_GATTC_READ_DESCR_EVT: + this->event_.gattc.data = new std::vector(p->read.value, p->read.value + p->read.value_len); + this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data(); + break; + default: + this->event_.gattc.data = nullptr; + break; + } + } + + // Initialize GATTS event data + void init_gatts_data_(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { + this->event_.gatts.gatts_event = e; + this->event_.gatts.gatts_if = i; + + if (p == nullptr) { + this->event_.gatts.gatts_param = nullptr; + this->event_.gatts.data = nullptr; + return; // Invalid event, but we can't log in header file + } + + // Heap-allocate param and data + // Heap allocation is used because GATTC/GATTS events are rare (<1% of events) + // while GAP events (99%) are stored inline to minimize memory usage + // IMPORTANT: This heap allocation provides clear ownership semantics: + // - The BLEEvent owns the allocated memory for its lifetime + // - The data remains valid from the BLE callback context until processed in the main loop + // - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory + this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p); + + // Copy data for events that need it + // The param struct contains pointers (e.g., write.value) that point to temporary buffers. + // We must copy this data to ensure it remains valid when the event is processed later. + switch (e) { + case ESP_GATTS_WRITE_EVT: + this->event_.gatts.data = new std::vector(p->write.value, p->write.value + p->write.len); + this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data(); + break; + default: + this->event_.gatts.data = nullptr; + break; + } + } }; // BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding) diff --git a/esphome/components/esp32_ble/ble_event_pool.h b/esphome/components/esp32_ble/ble_event_pool.h new file mode 100644 index 0000000000..ef123b1325 --- /dev/null +++ b/esphome/components/esp32_ble/ble_event_pool.h @@ -0,0 +1,72 @@ +#pragma once + +#ifdef USE_ESP32 + +#include +#include +#include "ble_event.h" +#include "queue.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace esp32_ble { + +// BLE Event Pool - On-demand pool of BLEEvent objects to avoid heap fragmentation +// Events are allocated on first use and reused thereafter, growing to peak usage +template class BLEEventPool { + public: + BLEEventPool() : total_created_(0) {} + + ~BLEEventPool() { + // Clean up any remaining events in the free list + BLEEvent *event; + while ((event = this->free_list_.pop()) != nullptr) { + delete event; + } + } + + // Allocate an event from the pool + // Returns nullptr if pool is full + BLEEvent *allocate() { + // Try to get from free list first + BLEEvent *event = this->free_list_.pop(); + if (event != nullptr) + return event; + + // Need to create a new event + if (this->total_created_ >= SIZE) { + // Pool is at capacity + return nullptr; + } + + // Use internal RAM for better performance + RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL); + event = allocator.allocate(1); + + if (event == nullptr) { + // Memory allocation failed + return nullptr; + } + + // Placement new to construct the object + new (event) BLEEvent(); + this->total_created_++; + return event; + } + + // Return an event to the pool for reuse + void release(BLEEvent *event) { + if (event != nullptr) { + this->free_list_.push(event); + } + } + + private: + LockFreeQueue free_list_; // Free events ready for reuse + uint8_t total_created_; // Total events created (high water mark) +}; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h index 56d2efd18b..75bf1eef25 100644 --- a/esphome/components/esp32_ble/queue.h +++ b/esphome/components/esp32_ble/queue.h @@ -18,7 +18,7 @@ namespace esphome { namespace esp32_ble { -template class LockFreeQueue { +template class LockFreeQueue { public: LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {} @@ -26,8 +26,8 @@ template class LockFreeQueue { if (element == nullptr) return false; - size_t current_tail = tail_.load(std::memory_order_relaxed); - size_t next_tail = (current_tail + 1) % SIZE; + uint8_t current_tail = tail_.load(std::memory_order_relaxed); + uint8_t next_tail = (current_tail + 1) % SIZE; if (next_tail == head_.load(std::memory_order_acquire)) { // Buffer full @@ -41,7 +41,7 @@ template class LockFreeQueue { } T *pop() { - size_t current_head = head_.load(std::memory_order_relaxed); + uint8_t current_head = head_.load(std::memory_order_relaxed); if (current_head == tail_.load(std::memory_order_acquire)) { return nullptr; // Empty @@ -53,27 +53,30 @@ template class LockFreeQueue { } size_t size() const { - size_t tail = tail_.load(std::memory_order_acquire); - size_t head = head_.load(std::memory_order_acquire); + uint8_t tail = tail_.load(std::memory_order_acquire); + uint8_t head = head_.load(std::memory_order_acquire); return (tail - head + SIZE) % SIZE; } - size_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } + uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); } bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); } bool full() const { - size_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; + uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; return next_tail == head_.load(std::memory_order_acquire); } protected: T *buffer_[SIZE]; - std::atomic head_; - std::atomic tail_; - std::atomic dropped_count_; + // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) + std::atomic dropped_count_; // 65535 max - more than enough for drop tracking + // Atomic: written by consumer (pop), read by producer (push) to check if full + std::atomic head_; + // Atomic: written by producer (push), read by consumer (pop) to check if empty + std::atomic tail_; }; } // namespace esp32_ble From 89b70e4352c80847c58d70d81626df10fc412c61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 21:37:21 +0200 Subject: [PATCH 056/293] Bump docker/setup-buildx-action from 3.11.0 to 3.11.1 in the docker-actions group (#9133) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-docker.yml | 2 +- .github/workflows/release.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 3bfed87237..f76ebba8e9 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -49,7 +49,7 @@ jobs: with: python-version: "3.10" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.11.0 + uses: docker/setup-buildx-action@v3.11.1 - name: Set TAG run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8239e03a99..b4518b27b5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -99,7 +99,7 @@ jobs: python-version: "3.10" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.11.0 + uses: docker/setup-buildx-action@v3.11.1 - name: Log in to docker hub uses: docker/login-action@v3.4.0 @@ -178,7 +178,7 @@ jobs: merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.11.0 + uses: docker/setup-buildx-action@v3.11.1 - name: Log in to docker hub if: matrix.registry == 'dockerhub' From f16f4e2c4ca72e707c485699f61794b93ade5f18 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 19:55:59 +0000 Subject: [PATCH 057/293] Bump aioesphomeapi from 32.2.3 to 32.2.4 (#9132) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 682f9dbe60..76a58bf622 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.8.1 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==32.2.3 +aioesphomeapi==32.2.4 zeroconf==0.147.0 puremagic==1.29 ruamel.yaml==0.18.14 # dashboard_import From 57388254c4ef7abce6a8108eb1d4c9118cb024db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 19:56:26 +0000 Subject: [PATCH 058/293] Bump pytest from 8.4.0 to 8.4.1 (#9131) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 8b42b9347c..9263d165ac 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,7 +5,7 @@ pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==8.4.0 +pytest==8.4.1 pytest-cov==6.2.1 pytest-mock==3.14.1 pytest-asyncio==1.0.0 From aa180b9581c715c3dce35da25fdd0d3d675336b9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:16:25 -0400 Subject: [PATCH 059/293] Bump ESP32 Arduino version to 3.1.3 (#8604) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kuba SzczodrzyÅ„ski --- esphome/components/ac_dimmer/ac_dimmer.cpp | 9 +- esphome/components/captive_portal/__init__.py | 1 + esphome/components/esp32/__init__.py | 32 ++++--- .../esp32_camera_web_server/__init__.py | 2 +- esphome/components/esp32_rmt/__init__.py | 58 ------------ .../esp32_rmt_led_strip/led_strip.cpp | 47 ---------- .../esp32_rmt_led_strip/led_strip.h | 15 ---- .../components/esp32_rmt_led_strip/light.py | 90 +++---------------- .../ethernet/ethernet_component.cpp | 2 +- esphome/components/http_request/__init__.py | 2 +- .../http_request/http_request_arduino.h | 1 + esphome/components/i2c/i2c_bus_arduino.cpp | 2 +- esphome/components/i2s_audio/i2s_audio.cpp | 2 +- .../i2s_audio/media_player/__init__.py | 2 +- esphome/components/ledc/ledc_output.cpp | 64 +------------ esphome/components/logger/__init__.py | 5 +- esphome/components/md5/md5.cpp | 6 +- esphome/components/md5/md5.h | 7 +- esphome/components/neopixelbus/light.py | 5 +- .../neopixelbus/neopixelbus_light.h | 2 +- esphome/components/network/__init__.py | 5 +- esphome/components/network/ip_address.h | 1 + esphome/components/nextion/display.py | 2 +- .../components/online_image/online_image.cpp | 2 +- .../components/remote_base/remote_base.cpp | 21 ----- esphome/components/remote_base/remote_base.h | 26 ------ .../components/remote_receiver/__init__.py | 67 +++++--------- .../remote_receiver/remote_receiver.h | 21 +---- .../remote_receiver/remote_receiver_esp32.cpp | 87 ------------------ .../components/remote_transmitter/__init__.py | 67 +++++--------- .../remote_transmitter/remote_transmitter.h | 16 +--- .../remote_transmitter_esp32.cpp | 77 ---------------- esphome/components/sntp/sntp_component.cpp | 6 +- .../wifi/wifi_component_esp32_arduino.cpp | 37 ++++---- esphome/core/defines.h | 2 +- platformio.ini | 11 ++- .../common-ard-esp32_rmt_led_strip.yaml | 1 - .../bluetooth_proxy/test.esp32-c3-ard.yaml | 8 -- ...p32-c3-idf.yaml => test.esp32-c6-idf.yaml} | 0 ....esp32-ard.yaml => test.esp32-s3-ard.yaml} | 0 ....esp32-idf.yaml => test.esp32-s3-idf.yaml} | 0 tests/components/e131/common-ard.yaml | 1 - .../esp32_camera_web_server/common.yaml | 4 + .../esp32_rmt_led_strip/common-ard.yaml | 18 ---- .../{common-idf.yaml => common.yaml} | 0 .../esp32_rmt_led_strip/test.esp32-ard.yaml | 2 +- .../test.esp32-c3-ard.yaml | 2 +- .../test.esp32-c3-idf.yaml | 2 +- .../esp32_rmt_led_strip/test.esp32-idf.yaml | 2 +- .../test.esp32-s3-idf.yaml | 2 +- .../test.esp32-s3-ard.yaml | 4 - tests/components/partition/common-ard.yaml | 1 - .../remote_receiver/esp32-common-ard.yaml | 14 --- ...sp32-common-idf.yaml => esp32-common.yaml} | 0 .../remote_receiver/test.esp32-ard.yaml | 7 +- .../remote_receiver/test.esp32-c3-ard.yaml | 7 +- .../remote_receiver/test.esp32-c3-idf.yaml | 2 +- .../remote_receiver/test.esp32-idf.yaml | 2 +- .../remote_receiver/test.esp32-s3-idf.yaml | 2 +- .../remote_transmitter/esp32-common-ard.yaml | 8 -- ...sp32-common-idf.yaml => esp32-common.yaml} | 0 .../remote_transmitter/test.esp32-ard.yaml | 5 +- .../remote_transmitter/test.esp32-c3-ard.yaml | 5 +- .../remote_transmitter/test.esp32-c3-idf.yaml | 2 +- .../remote_transmitter/test.esp32-idf.yaml | 2 +- .../remote_transmitter/test.esp32-s3-idf.yaml | 2 +- tests/components/wled/test.esp32-ard.yaml | 1 - tests/components/wled/test.esp32-c3-ard.yaml | 1 - 68 files changed, 171 insertions(+), 738 deletions(-) delete mode 100644 tests/components/bluetooth_proxy/test.esp32-c3-ard.yaml rename tests/components/bluetooth_proxy/{test.esp32-c3-idf.yaml => test.esp32-c6-idf.yaml} (100%) rename tests/components/bluetooth_proxy/{test.esp32-ard.yaml => test.esp32-s3-ard.yaml} (100%) rename tests/components/bluetooth_proxy/{test.esp32-idf.yaml => test.esp32-s3-idf.yaml} (100%) delete mode 100644 tests/components/esp32_rmt_led_strip/common-ard.yaml rename tests/components/esp32_rmt_led_strip/{common-idf.yaml => common.yaml} (100%) delete mode 100644 tests/components/remote_receiver/esp32-common-ard.yaml rename tests/components/remote_receiver/{esp32-common-idf.yaml => esp32-common.yaml} (100%) delete mode 100644 tests/components/remote_transmitter/esp32-common-ard.yaml rename tests/components/remote_transmitter/{esp32-common-idf.yaml => esp32-common.yaml} (100%) diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index ddaa910db3..276adeebb0 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -193,14 +193,13 @@ void AcDimmer::setup() { setTimer1Callback(&timer_interrupt); #endif #ifdef USE_ESP32 - // 80 Divider -> 1 count=1µs - dimmer_timer = timerBegin(0, 80, true); - timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr, true); + // timer frequency of 1mhz + dimmer_timer = timerBegin(1000000); + timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr); // For ESP32, we can't use dynamic interval calculation because the timerX functions // are not callable from ISR (placed in flash storage). // Here we just use an interrupt firing every 50 µs. - timerAlarmWrite(dimmer_timer, 50, true); - timerAlarmEnable(dimmer_timer); + timerAlarm(dimmer_timer, 50, true, 0); #endif } void AcDimmer::write_state(float state) { diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index ea11e733ac..a55887948d 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -41,6 +41,7 @@ async def to_code(config): if CORE.using_arduino: if CORE.is_esp32: + cg.add_library("ESP32 Async UDP", None) cg.add_library("DNSServer", None) cg.add_library("WiFi", None) if CORE.is_esp8266: diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 7f2d718d35..f179c315f9 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -132,6 +132,8 @@ def set_core_data(config): choices = CPU_FREQUENCIES[variant] if "160MHZ" in choices: cpu_frequency = "160MHZ" + elif "360MHZ" in choices: + cpu_frequency = "360MHZ" else: cpu_frequency = choices[-1] config[CONF_CPU_FREQUENCY] = cpu_frequency @@ -289,11 +291,8 @@ def add_extra_build_file(filename: str, path: str) -> bool: def _format_framework_arduino_version(ver: cv.Version) -> str: # format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to - # a PIO platformio/framework-arduinoespressif32 value - # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif32 - if ver <= cv.Version(1, 0, 3): - return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" - return f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" + # a PIO pioarduino/framework-arduinoespressif32 value + return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip" def _format_framework_espidf_version( @@ -317,12 +316,10 @@ def _format_framework_espidf_version( # The default/recommended arduino framework version # - https://github.com/espressif/arduino-esp32/releases -# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif32 -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 0, 5) -# The platformio/espressif32 version to use for arduino frameworks -# - https://github.com/platformio/platform-espressif32/releases -# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 -ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0) +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 3) +# The platform-espressif32 version to use for arduino frameworks +# - https://github.com/pioarduino/platform-espressif32/releases +ARDUINO_PLATFORM_VERSION = cv.Version(53, 3, 13) # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases @@ -365,8 +362,8 @@ SUPPORTED_PIOARDUINO_ESP_IDF_5X = [ def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(2, 1, 0), "https://github.com/espressif/arduino-esp32.git"), - "latest": (cv.Version(2, 0, 9), None), + "dev": (cv.Version(3, 1, 3), "https://github.com/espressif/arduino-esp32.git"), + "latest": (cv.Version(3, 1, 3), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } @@ -388,6 +385,10 @@ def _arduino_check_versions(value): CONF_PLATFORM_VERSION, _parse_platform_version(str(ARDUINO_PLATFORM_VERSION)) ) + if value[CONF_SOURCE].startswith("http"): + # prefix is necessary or platformio will complain with a cryptic error + value[CONF_SOURCE] = f"framework-arduinoespressif32@{value[CONF_SOURCE]}" + if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: _LOGGER.warning( "The selected Arduino framework version is not the recommended one. " @@ -829,10 +830,7 @@ async def to_code(config): cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") - cg.add_platformio_option( - "platform_packages", - [f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"], - ) + cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]]) if CONF_PARTITIONS in config: cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS]) diff --git a/esphome/components/esp32_camera_web_server/__init__.py b/esphome/components/esp32_camera_web_server/__init__.py index 363218bbac..df137c8ff2 100644 --- a/esphome/components/esp32_camera_web_server/__init__.py +++ b/esphome/components/esp32_camera_web_server/__init__.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_MODE, CONF_PORT CODEOWNERS = ["@ayufan"] -DEPENDENCIES = ["esp32_camera"] +DEPENDENCIES = ["esp32_camera", "network"] MULTI_CONF = True esp32_camera_web_server_ns = cg.esphome_ns.namespace("esp32_camera_web_server") diff --git a/esphome/components/esp32_rmt/__init__.py b/esphome/components/esp32_rmt/__init__.py index 171c335727..1e72185e3e 100644 --- a/esphome/components/esp32_rmt/__init__.py +++ b/esphome/components/esp32_rmt/__init__.py @@ -1,48 +1,8 @@ -import esphome.codegen as cg from esphome.components import esp32 import esphome.config_validation as cv -from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION -from esphome.core import CORE CODEOWNERS = ["@jesserockz"] -RMT_TX_CHANNELS = { - esp32.const.VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7], - esp32.const.VARIANT_ESP32S2: [0, 1, 2, 3], - esp32.const.VARIANT_ESP32S3: [0, 1, 2, 3], - esp32.const.VARIANT_ESP32C3: [0, 1], - esp32.const.VARIANT_ESP32C6: [0, 1], - esp32.const.VARIANT_ESP32H2: [0, 1], -} - -RMT_RX_CHANNELS = { - esp32.const.VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7], - esp32.const.VARIANT_ESP32S2: [0, 1, 2, 3], - esp32.const.VARIANT_ESP32S3: [4, 5, 6, 7], - esp32.const.VARIANT_ESP32C3: [2, 3], - esp32.const.VARIANT_ESP32C6: [2, 3], - esp32.const.VARIANT_ESP32H2: [2, 3], -} - -rmt_channel_t = cg.global_ns.enum("rmt_channel_t") -RMT_CHANNEL_ENUMS = { - 0: rmt_channel_t.RMT_CHANNEL_0, - 1: rmt_channel_t.RMT_CHANNEL_1, - 2: rmt_channel_t.RMT_CHANNEL_2, - 3: rmt_channel_t.RMT_CHANNEL_3, - 4: rmt_channel_t.RMT_CHANNEL_4, - 5: rmt_channel_t.RMT_CHANNEL_5, - 6: rmt_channel_t.RMT_CHANNEL_6, - 7: rmt_channel_t.RMT_CHANNEL_7, -} - - -def use_new_rmt_driver(): - framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] - if CORE.using_esp_idf and framework_version >= cv.Version(5, 0, 0): - return True - return False - def validate_clock_resolution(): def _validator(value): @@ -60,21 +20,3 @@ def validate_clock_resolution(): return value return _validator - - -def validate_rmt_channel(*, tx: bool): - rmt_channels = RMT_TX_CHANNELS if tx else RMT_RX_CHANNELS - - def _validator(value): - cv.only_on_esp32(value) - value = cv.int_(value) - variant = esp32.get_esp32_variant() - if variant not in rmt_channels: - raise cv.Invalid(f"ESP32 variant {variant} does not support RMT.") - if value not in rmt_channels[variant]: - raise cv.Invalid( - f"RMT channel {value} does not support {'transmitting' if tx else 'receiving'} for ESP32 variant {variant}." - ) - return cv.enum(RMT_CHANNEL_ENUMS)(value) - - return _validator diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index 88ddf24d49..dfdf50aa66 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -42,7 +42,6 @@ void ESP32RMTLEDStripLightOutput::setup() { return; } -#if ESP_IDF_VERSION_MAJOR >= 5 RAMAllocator rmt_allocator(this->use_psram_ ? 0 : RAMAllocator::ALLOC_INTERNAL); // 8 bits per byte, 1 rmt_symbol_word_t per bit + 1 rmt_symbol_word_t for reset @@ -79,36 +78,6 @@ void ESP32RMTLEDStripLightOutput::setup() { this->mark_failed(); return; } -#else - RAMAllocator rmt_allocator(this->use_psram_ ? 0 : RAMAllocator::ALLOC_INTERNAL); - - // 8 bits per byte, 1 rmt_item32_t per bit + 1 rmt_item32_t for reset - this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + 1); - - rmt_config_t config; - memset(&config, 0, sizeof(config)); - config.channel = this->channel_; - config.rmt_mode = RMT_MODE_TX; - config.gpio_num = gpio_num_t(this->pin_); - config.mem_block_num = 1; - config.clk_div = RMT_CLK_DIV; - config.tx_config.loop_en = false; - config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; - config.tx_config.carrier_en = false; - config.tx_config.idle_level = RMT_IDLE_LEVEL_LOW; - config.tx_config.idle_output_en = true; - - if (rmt_config(&config) != ESP_OK) { - ESP_LOGE(TAG, "Cannot initialize RMT!"); - this->mark_failed(); - return; - } - if (rmt_driver_install(config.channel, 0, 0) != ESP_OK) { - ESP_LOGE(TAG, "Cannot install RMT driver!"); - this->mark_failed(); - return; - } -#endif } void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, @@ -145,11 +114,7 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { ESP_LOGVV(TAG, "Writing RGB values to bus"); -#if ESP_IDF_VERSION_MAJOR >= 5 esp_err_t error = rmt_tx_wait_all_done(this->channel_, 1000); -#else - esp_err_t error = rmt_wait_tx_done(this->channel_, pdMS_TO_TICKS(1000)); -#endif if (error != ESP_OK) { ESP_LOGE(TAG, "RMT TX timeout"); this->status_set_warning(); @@ -162,11 +127,7 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { size_t size = 0; size_t len = 0; uint8_t *psrc = this->buf_; -#if ESP_IDF_VERSION_MAJOR >= 5 rmt_symbol_word_t *pdest = this->rmt_buf_; -#else - rmt_item32_t *pdest = this->rmt_buf_; -#endif while (size < buffer_size) { uint8_t b = *psrc; for (int i = 0; i < 8; i++) { @@ -184,15 +145,11 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { len++; } -#if ESP_IDF_VERSION_MAJOR >= 5 rmt_transmit_config_t config; memset(&config, 0, sizeof(config)); config.loop_count = 0; config.flags.eot_level = 0; error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, len * sizeof(rmt_symbol_word_t), &config); -#else - error = rmt_write_items(this->channel_, this->rmt_buf_, len, false); -#endif if (error != ESP_OK) { ESP_LOGE(TAG, "RMT TX error"); this->status_set_warning(); @@ -251,11 +208,7 @@ void ESP32RMTLEDStripLightOutput::dump_config() { "ESP32 RMT LED Strip:\n" " Pin: %u", this->pin_); -#if ESP_IDF_VERSION_MAJOR >= 5 ESP_LOGCONFIG(TAG, " RMT Symbols: %" PRIu32, this->rmt_symbols_); -#else - ESP_LOGCONFIG(TAG, " Channel: %u", this->channel_); -#endif const char *rgb_order; switch (this->rgb_order_) { case ORDER_RGB: diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.h b/esphome/components/esp32_rmt_led_strip/led_strip.h index f0cec9b291..c6a2b4bc9f 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.h +++ b/esphome/components/esp32_rmt_led_strip/led_strip.h @@ -11,12 +11,7 @@ #include #include #include - -#if ESP_IDF_VERSION_MAJOR >= 5 #include -#else -#include -#endif namespace esphome { namespace esp32_rmt_led_strip { @@ -61,11 +56,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { uint32_t reset_time_high, uint32_t reset_time_low); void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; } -#if ESP_IDF_VERSION_MAJOR >= 5 void set_rmt_symbols(uint32_t rmt_symbols) { this->rmt_symbols_ = rmt_symbols; } -#else - void set_rmt_channel(rmt_channel_t channel) { this->channel_ = channel; } -#endif void clear_effect_data() override { for (int i = 0; i < this->size(); i++) @@ -81,17 +72,11 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { uint8_t *buf_{nullptr}; uint8_t *effect_data_{nullptr}; -#if ESP_IDF_VERSION_MAJOR >= 5 rmt_channel_handle_t channel_{nullptr}; rmt_encoder_handle_t encoder_{nullptr}; rmt_symbol_word_t *rmt_buf_{nullptr}; rmt_symbol_word_t bit0_, bit1_, reset_; uint32_t rmt_symbols_{48}; -#else - rmt_item32_t *rmt_buf_{nullptr}; - rmt_item32_t bit0_, bit1_, reset_; - rmt_channel_t channel_{RMT_CHANNEL_0}; -#endif uint8_t pin_; uint16_t num_leds_; diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 596770b96d..33ae44e435 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -3,7 +3,7 @@ import logging from esphome import pins import esphome.codegen as cg -from esphome.components import esp32, esp32_rmt, light +from esphome.components import esp32, light import esphome.config_validation as cv from esphome.const import ( CONF_CHIPSET, @@ -13,11 +13,9 @@ from esphome.const import ( CONF_OUTPUT_ID, CONF_PIN, CONF_RGB_ORDER, - CONF_RMT_CHANNEL, CONF_RMT_SYMBOLS, CONF_USE_DMA, ) -from esphome.core import CORE _LOGGER = logging.getLogger(__name__) @@ -69,53 +67,6 @@ CONF_RESET_HIGH = "reset_high" CONF_RESET_LOW = "reset_low" -class OptionalForIDF5(cv.SplitDefault): - @property - def default(self): - if not esp32_rmt.use_new_rmt_driver(): - return cv.UNDEFINED - return super().default - - @default.setter - def default(self, value): - # Ignore default set from vol.Optional - pass - - -def only_with_new_rmt_driver(obj): - if not esp32_rmt.use_new_rmt_driver(): - raise cv.Invalid( - "This feature is only available for the IDF framework version 5." - ) - return obj - - -def not_with_new_rmt_driver(obj): - if esp32_rmt.use_new_rmt_driver(): - raise cv.Invalid( - "This feature is not available for the IDF framework version 5." - ) - return obj - - -def final_validation(config): - if not esp32_rmt.use_new_rmt_driver(): - if CONF_RMT_CHANNEL not in config: - if CORE.using_esp_idf: - raise cv.Invalid( - "rmt_channel is a required option for IDF version < 5." - ) - raise cv.Invalid( - "rmt_channel is a required option for the Arduino framework." - ) - _LOGGER.warning( - "RMT_LED_STRIP support for IDF version < 5 is deprecated and will be removed soon." - ) - - -FINAL_VALIDATE_SCHEMA = final_validation - - CONFIG_SCHEMA = cv.All( light.ADDRESSABLE_LIGHT_SCHEMA.extend( { @@ -123,20 +74,17 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), - cv.Optional(CONF_RMT_CHANNEL): cv.All( - not_with_new_rmt_driver, esp32_rmt.validate_rmt_channel(tx=True) - ), - OptionalForIDF5( + cv.SplitDefault( CONF_RMT_SYMBOLS, - esp32_idf=192, - esp32_s2_idf=192, - esp32_s3_idf=192, - esp32_p4_idf=192, - esp32_c3_idf=96, - esp32_c5_idf=96, - esp32_c6_idf=96, - esp32_h2_idf=96, - ): cv.All(only_with_new_rmt_driver, cv.int_range(min=2)), + esp32=192, + esp32_s2=192, + esp32_s3=192, + esp32_p4=192, + esp32_c3=96, + esp32_c5=96, + esp32_c6=96, + esp32_h2=96, + ): cv.int_range(min=2), cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, @@ -145,7 +93,6 @@ CONFIG_SCHEMA = cv.All( esp32.only_on_variant( supported=[esp32.const.VARIANT_ESP32S3, esp32.const.VARIANT_ESP32P4] ), - cv.only_with_esp_idf, cv.boolean, ), cv.Optional(CONF_USE_PSRAM, default=True): cv.boolean, @@ -218,15 +165,6 @@ async def to_code(config): cg.add(var.set_is_rgbw(config[CONF_IS_RGBW])) cg.add(var.set_is_wrgb(config[CONF_IS_WRGB])) cg.add(var.set_use_psram(config[CONF_USE_PSRAM])) - - if esp32_rmt.use_new_rmt_driver(): - cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) - if CONF_USE_DMA in config: - cg.add(var.set_use_dma(config[CONF_USE_DMA])) - else: - rmt_channel_t = cg.global_ns.enum("rmt_channel_t") - cg.add( - var.set_rmt_channel( - getattr(rmt_channel_t, f"RMT_CHANNEL_{config[CONF_RMT_CHANNEL]}") - ) - ) + cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) + if CONF_USE_DMA in config: + cg.add(var.set_use_dma(config[CONF_USE_DMA])) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index fe96973924..0a6ba6470e 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -106,7 +106,7 @@ void EthernetComponent::setup() { .post_cb = nullptr, }; -#if USE_ESP_IDF && (ESP_IDF_VERSION_MAJOR >= 5) +#if ESP_IDF_VERSION_MAJOR >= 5 eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg); #else spi_device_handle_t spi_handle = nullptr; diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index ac13334118..18373edb77 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -175,7 +175,7 @@ async def to_code(config): not config.get(CONF_VERIFY_SSL), ) else: - cg.add_library("WiFiClientSecure", None) + cg.add_library("NetworkClientSecure", None) cg.add_library("HTTPClient", None) if CORE.is_esp8266: cg.add_library("ESP8266HTTPClient", None) diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index ac9ddffbb0..44744f8c78 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -6,6 +6,7 @@ #if defined(USE_ESP32) || defined(USE_RP2040) #include +#include #endif #ifdef USE_ESP8266 #include diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index dca77e878d..e9d8c2415c 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -125,7 +125,7 @@ ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) size_t to_request = 0; for (size_t i = 0; i < cnt; i++) to_request += buffers[i].len; - size_t ret = wire_->requestFrom((int) address, (int) to_request, 1); + size_t ret = wire_->requestFrom(address, to_request, true); if (ret != to_request) { ESP_LOGVV(TAG, "RX %u from %02X failed with error %u", to_request, address, ret); return ERROR_TIMEOUT; diff --git a/esphome/components/i2s_audio/i2s_audio.cpp b/esphome/components/i2s_audio/i2s_audio.cpp index 0f2995b4bd..7ff21bba57 100644 --- a/esphome/components/i2s_audio/i2s_audio.cpp +++ b/esphome/components/i2s_audio/i2s_audio.cpp @@ -9,7 +9,7 @@ namespace i2s_audio { static const char *const TAG = "i2s_audio"; -#if defined(USE_ESP_IDF) && (ESP_IDF_VERSION_MAJOR >= 5) +#if ESP_IDF_VERSION_MAJOR >= 5 static const uint8_t I2S_NUM_MAX = SOC_I2S_NUM; // because IDF 5+ took this away :( #endif diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index 8797d13e7c..ad6665a5f5 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -114,7 +114,7 @@ async def to_code(config): cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb")) - cg.add_library("WiFiClientSecure", None) + cg.add_library("NetworkClientSecure", None) cg.add_library("HTTPClient", None) cg.add_library("esphome/ESP32-audioI2S", "2.3.0") cg.add_build_flag("-DAUDIO_NO_SD_FS") diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index aefe0e63d8..2ae2656f54 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -3,28 +3,16 @@ #ifdef USE_ESP32 -#ifdef USE_ARDUINO -#include -#endif #include - #include #define CLOCK_FREQUENCY 80e6f -#ifdef USE_ARDUINO -#ifdef SOC_LEDC_SUPPORT_XTAL_CLOCK -#undef CLOCK_FREQUENCY -// starting with ESP32 Arduino 2.0.2, the 40MHz crystal is used as clock by default if supported -#define CLOCK_FREQUENCY 40e6f -#endif -#else #ifdef SOC_LEDC_SUPPORT_APB_CLOCK #define DEFAULT_CLK LEDC_USE_APB_CLK #else #define DEFAULT_CLK LEDC_AUTO_CLK #endif -#endif static const uint8_t SETUP_ATTEMPT_COUNT_MAX = 5; @@ -34,7 +22,6 @@ namespace ledc { static const char *const TAG = "ledc.output"; static const int MAX_RES_BITS = LEDC_TIMER_BIT_MAX - 1; -#ifdef USE_ESP_IDF #if SOC_LEDC_SUPPORT_HS_MODE // Only ESP32 has LEDC_HIGH_SPEED_MODE inline ledc_mode_t get_speed_mode(uint8_t channel) { return channel < 8 ? LEDC_HIGH_SPEED_MODE : LEDC_LOW_SPEED_MODE; } @@ -44,7 +31,6 @@ inline ledc_mode_t get_speed_mode(uint8_t channel) { return channel < 8 ? LEDC_H // https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/api-reference/peripherals/ledc.html#functionality-overview inline ledc_mode_t get_speed_mode(uint8_t) { return LEDC_LOW_SPEED_MODE; } #endif -#endif float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) { return static_cast(CLOCK_FREQUENCY) / static_cast(1 << bit_depth); @@ -68,7 +54,6 @@ optional ledc_bit_depth_for_frequency(float frequency) { return {}; } -#ifdef USE_ESP_IDF esp_err_t configure_timer_frequency(ledc_mode_t speed_mode, ledc_timer_t timer_num, ledc_channel_t chan_num, uint8_t channel, uint8_t &bit_depth, float frequency) { bit_depth = *ledc_bit_depth_for_frequency(frequency); @@ -98,13 +83,10 @@ esp_err_t configure_timer_frequency(ledc_mode_t speed_mode, ledc_timer_t timer_n return init_result; } -#endif -#ifdef USE_ESP_IDF constexpr int ledc_angle_to_htop(float angle, uint8_t bit_depth) { return static_cast(angle * ((1U << bit_depth) - 1) / 360.0f); } -#endif // USE_ESP_IDF void LEDCOutput::write_state(float state) { if (!this->initialized_) { @@ -120,10 +102,6 @@ void LEDCOutput::write_state(float state) { const float duty_rounded = roundf(state * max_duty); auto duty = static_cast(duty_rounded); ESP_LOGV(TAG, "Setting duty: %" PRIu32 " on channel %u", duty, this->channel_); -#ifdef USE_ARDUINO - ledcWrite(this->channel_, duty); -#endif -#ifdef USE_ESP_IDF auto speed_mode = get_speed_mode(this->channel_); auto chan_num = static_cast(this->channel_ % 8); int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_); @@ -135,18 +113,10 @@ void LEDCOutput::write_state(float state) { ledc_set_duty_with_hpoint(speed_mode, chan_num, duty, hpoint); ledc_update_duty(speed_mode, chan_num); } -#endif } void LEDCOutput::setup() { ESP_LOGCONFIG(TAG, "Running setup"); -#ifdef USE_ARDUINO - this->update_frequency(this->frequency_); - this->turn_off(); - // Attach pin after setting default value - ledcAttachPin(this->pin_->get_pin(), this->channel_); -#endif -#ifdef USE_ESP_IDF auto speed_mode = get_speed_mode(this->channel_); auto timer_num = static_cast((this->channel_ % 8) / 2); auto chan_num = static_cast(this->channel_ % 8); @@ -175,7 +145,6 @@ void LEDCOutput::setup() { ledc_channel_config(&chan_conf); this->initialized_ = true; this->status_clear_error(); -#endif } void LEDCOutput::dump_config() { @@ -208,38 +177,7 @@ void LEDCOutput::update_frequency(float frequency) { } this->bit_depth_ = bit_depth_opt.value_or(8); this->frequency_ = frequency; -#ifdef USE_ARDUINO - ESP_LOGV(TAG, "Using Arduino API - Trying to define channel, frequency and bit depth"); - u_int32_t configured_frequency = 0; - // Configure LEDC channel, frequency and bit depth with fallback - int attempt_count_max = SETUP_ATTEMPT_COUNT_MAX; - while (attempt_count_max > 0 && configured_frequency == 0) { - ESP_LOGV(TAG, "Initializing channel %u with frequency %.1f and bit depth of %u", this->channel_, this->frequency_, - this->bit_depth_); - configured_frequency = ledcSetup(this->channel_, frequency, this->bit_depth_); - if (configured_frequency != 0) { - this->initialized_ = true; - this->status_clear_error(); - ESP_LOGV(TAG, "Configured frequency: %u with bit depth: %u", configured_frequency, this->bit_depth_); - } else { - ESP_LOGW(TAG, "Unable to initialize channel %u with frequency %.1f and bit depth of %u", this->channel_, - this->frequency_, this->bit_depth_); - // try again with a lower bit depth - this->bit_depth_--; - } - attempt_count_max--; - } - - if (configured_frequency == 0) { - ESP_LOGE(TAG, "Permanently failed to initialize channel %u with frequency %.1f and bit depth of %u", this->channel_, - this->frequency_, this->bit_depth_); - this->status_set_error(); - return; - } - -#endif // USE_ARDUINO -#ifdef USE_ESP_IDF if (!this->initialized_) { ESP_LOGW(TAG, "Not yet initialized"); return; @@ -259,7 +197,7 @@ void LEDCOutput::update_frequency(float frequency) { } this->status_clear_error(); -#endif + // re-apply duty this->write_state(this->duty_); } diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 462cae73b6..26516e1506 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -324,7 +324,10 @@ async def to_code(config): if CORE.using_arduino: if config[CONF_HARDWARE_UART] == USB_CDC: cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=1") - if CORE.is_esp32 and get_esp32_variant() == VARIANT_ESP32C3: + if CORE.is_esp32 and get_esp32_variant() in ( + VARIANT_ESP32C3, + VARIANT_ESP32C6, + ): cg.add_build_flag("-DARDUINO_USB_MODE=1") if CORE.using_esp_idf: diff --git a/esphome/components/md5/md5.cpp b/esphome/components/md5/md5.cpp index 31f52634be..980cb98699 100644 --- a/esphome/components/md5/md5.cpp +++ b/esphome/components/md5/md5.cpp @@ -7,7 +7,7 @@ namespace esphome { namespace md5 { -#if defined(USE_ARDUINO) && !defined(USE_RP2040) +#if defined(USE_ARDUINO) && !defined(USE_RP2040) && !defined(USE_ESP32) void MD5Digest::init() { memset(this->digest_, 0, 16); MD5Init(&this->ctx_); @@ -18,7 +18,7 @@ void MD5Digest::add(const uint8_t *data, size_t len) { MD5Update(&this->ctx_, da void MD5Digest::calculate() { MD5Final(this->digest_, &this->ctx_); } #endif // USE_ARDUINO && !USE_RP2040 -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 void MD5Digest::init() { memset(this->digest_, 0, 16); esp_rom_md5_init(&this->ctx_); @@ -27,7 +27,7 @@ void MD5Digest::init() { void MD5Digest::add(const uint8_t *data, size_t len) { esp_rom_md5_update(&this->ctx_, data, len); } void MD5Digest::calculate() { esp_rom_md5_final(this->digest_, &this->ctx_); } -#endif // USE_ESP_IDF +#endif // USE_ESP32 #ifdef USE_RP2040 void MD5Digest::init() { diff --git a/esphome/components/md5/md5.h b/esphome/components/md5/md5.h index cb6accf46f..be1df40423 100644 --- a/esphome/components/md5/md5.h +++ b/esphome/components/md5/md5.h @@ -3,16 +3,11 @@ #include "esphome/core/defines.h" #ifdef USE_MD5 -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esp_rom_md5.h" #define MD5_CTX_TYPE md5_context_t #endif -#if defined(USE_ARDUINO) && defined(USE_ESP32) -#include "rom/md5_hash.h" -#define MD5_CTX_TYPE MD5Context -#endif - #if defined(USE_ARDUINO) && defined(USE_ESP8266) #include #define MD5_CTX_TYPE md5_context_t diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index affeb2de8f..3cd1bfd357 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -215,4 +215,7 @@ async def to_code(config): # https://github.com/Makuna/NeoPixelBus/blob/master/library.json # Version Listed Here: https://registry.platformio.org/libraries/makuna/NeoPixelBus/versions - cg.add_library("makuna/NeoPixelBus", "2.7.3") + if CORE.is_esp32: + cg.add_library("makuna/NeoPixelBus", "2.8.0") + else: + cg.add_library("makuna/NeoPixelBus", "2.7.3") diff --git a/esphome/components/neopixelbus/neopixelbus_light.h b/esphome/components/neopixelbus/neopixelbus_light.h index d94a923614..c27244b94d 100644 --- a/esphome/components/neopixelbus/neopixelbus_light.h +++ b/esphome/components/neopixelbus/neopixelbus_light.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(CLANG_TIDY) #include "esphome/core/color.h" #include "esphome/core/component.h" diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 129b1ced06..b04fca7a1c 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -2,7 +2,7 @@ import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option import esphome.config_validation as cv from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT -from esphome.core import CORE +from esphome.core import CORE, coroutine_with_priority CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["mdns"] @@ -36,8 +36,11 @@ CONFIG_SCHEMA = cv.Schema( ) +@coroutine_with_priority(201.0) async def to_code(config): cg.add_define("USE_NETWORK") + if CORE.using_arduino and CORE.is_esp32: + cg.add_library("Networking", None) if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None: cg.add_define("USE_NETWORK_IPV6", enable_ipv6) if enable_ipv6: diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index d76da573b5..5e6b0dbd96 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -56,6 +56,7 @@ struct IPAddress { IP_ADDR4(&ip_addr_, first, second, third, fourth); } IPAddress(const ip_addr_t *other_ip) { ip_addr_copy(ip_addr_, *other_ip); } + IPAddress(const char *in_address) { ipaddr_aton(in_address, &ip_addr_); } IPAddress(const std::string &in_address) { ipaddr_aton(in_address.c_str(), &ip_addr_); } IPAddress(ip4_addr_t *other_ip) { memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip4_addr_t)); diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 7f63ca147b..0aa5efeba7 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -150,7 +150,7 @@ async def to_code(config): cg.add_define("USE_NEXTION_TFT_UPLOAD") cg.add(var.set_tft_url(config[CONF_TFT_URL])) if CORE.is_esp32 and CORE.using_arduino: - cg.add_library("WiFiClientSecure", None) + cg.add_library("NetworkClientSecure", None) cg.add_library("HTTPClient", None) elif CORE.is_esp32 and CORE.using_esp_idf: esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True) diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 3f1d58fb45..e21b2528d5 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -224,7 +224,7 @@ void OnlineImage::loop() { this->height_ = buffer_height_; ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(), this->width_, this->height_); - ESP_LOGD(TAG, "Total time: %lds", ::time(nullptr) - this->start_time_); + ESP_LOGD(TAG, "Total time: %" PRIu32 "s", (uint32_t) (::time(nullptr) - this->start_time_)); this->etag_ = this->downloader_->get_response_header(ETAG_HEADER_NAME); this->last_modified_ = this->downloader_->get_response_header(LAST_MODIFIED_HEADER_NAME); this->download_finished_callback_.call(false); diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 5dff2c6a38..987286b345 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -8,27 +8,6 @@ namespace remote_base { static const char *const TAG = "remote_base"; -#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR < 5 -RemoteRMTChannel::RemoteRMTChannel(uint8_t mem_block_num) : mem_block_num_(mem_block_num) { - static rmt_channel_t next_rmt_channel = RMT_CHANNEL_0; - this->channel_ = next_rmt_channel; - next_rmt_channel = rmt_channel_t(int(next_rmt_channel) + mem_block_num); -} - -RemoteRMTChannel::RemoteRMTChannel(rmt_channel_t channel, uint8_t mem_block_num) - : channel_(channel), mem_block_num_(mem_block_num) {} - -void RemoteRMTChannel::config_rmt(rmt_config_t &rmt) { - if (rmt_channel_t(int(this->channel_) + this->mem_block_num_) > RMT_CHANNEL_MAX) { - this->mem_block_num_ = int(RMT_CHANNEL_MAX) - int(this->channel_); - ESP_LOGW(TAG, "Not enough RMT memory blocks available, reduced to %i blocks.", this->mem_block_num_); - } - rmt.channel = this->channel_; - rmt.clk_div = this->clock_divider_; - rmt.mem_block_num = this->mem_block_num_; -} -#endif - /* RemoteReceiveData */ bool RemoteReceiveData::peek_mark(uint32_t length, uint32_t offset) const { diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 4131d080f5..a18dd0ed7e 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -8,10 +8,6 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR < 5 -#include -#endif - namespace esphome { namespace remote_base { @@ -112,43 +108,21 @@ class RemoteComponentBase { #ifdef USE_ESP32 class RemoteRMTChannel { public: -#if ESP_IDF_VERSION_MAJOR >= 5 void set_clock_resolution(uint32_t clock_resolution) { this->clock_resolution_ = clock_resolution; } void set_rmt_symbols(uint32_t rmt_symbols) { this->rmt_symbols_ = rmt_symbols; } -#else - explicit RemoteRMTChannel(uint8_t mem_block_num = 1); - explicit RemoteRMTChannel(rmt_channel_t channel, uint8_t mem_block_num = 1); - - void config_rmt(rmt_config_t &rmt); - void set_clock_divider(uint8_t clock_divider) { this->clock_divider_ = clock_divider; } -#endif protected: uint32_t from_microseconds_(uint32_t us) { -#if ESP_IDF_VERSION_MAJOR >= 5 const uint32_t ticks_per_ten_us = this->clock_resolution_ / 100000u; -#else - const uint32_t ticks_per_ten_us = 80000000u / this->clock_divider_ / 100000u; -#endif return us * ticks_per_ten_us / 10; } uint32_t to_microseconds_(uint32_t ticks) { -#if ESP_IDF_VERSION_MAJOR >= 5 const uint32_t ticks_per_ten_us = this->clock_resolution_ / 100000u; -#else - const uint32_t ticks_per_ten_us = 80000000u / this->clock_divider_ / 100000u; -#endif return (ticks * 10) / ticks_per_ten_us; } RemoteComponentBase *remote_base_; -#if ESP_IDF_VERSION_MAJOR >= 5 uint32_t clock_resolution_{1000000}; uint32_t rmt_symbols_; -#else - rmt_channel_t channel_{RMT_CHANNEL_0}; - uint8_t mem_block_num_; - uint8_t clock_divider_{80}; -#endif }; #endif diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 6994eebd91..321cfc93ff 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -4,15 +4,12 @@ from esphome.components import esp32, esp32_rmt, remote_base import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, - CONF_CLOCK_DIVIDER, CONF_CLOCK_RESOLUTION, CONF_DUMP, CONF_FILTER, CONF_ID, CONF_IDLE, - CONF_MEMORY_BLOCKS, CONF_PIN, - CONF_RMT_CHANNEL, CONF_RMT_SYMBOLS, CONF_TOLERANCE, CONF_TYPE, @@ -103,49 +100,36 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.positive_time_period_microseconds, cv.Range(max=TimePeriod(microseconds=4294967295)), ), - cv.SplitDefault(CONF_CLOCK_DIVIDER, esp32_arduino=80): cv.All( - cv.only_on_esp32, - cv.only_with_arduino, - cv.int_range(min=1, max=255), - ), cv.Optional(CONF_CLOCK_RESOLUTION): cv.All( cv.only_on_esp32, - cv.only_with_esp_idf, esp32_rmt.validate_clock_resolution(), ), cv.Optional(CONF_IDLE, default="10ms"): cv.All( cv.positive_time_period_microseconds, cv.Range(max=TimePeriod(microseconds=4294967295)), ), - cv.SplitDefault(CONF_MEMORY_BLOCKS, esp32_arduino=3): cv.All( - cv.only_with_arduino, cv.int_range(min=1, max=8) - ), - cv.Optional(CONF_RMT_CHANNEL): cv.All( - cv.only_with_arduino, esp32_rmt.validate_rmt_channel(tx=False) - ), cv.SplitDefault( CONF_RMT_SYMBOLS, - esp32_idf=192, - esp32_s2_idf=192, - esp32_s3_idf=192, - esp32_p4_idf=192, - esp32_c3_idf=96, - esp32_c5_idf=96, - esp32_c6_idf=96, - esp32_h2_idf=96, - ): cv.All(cv.only_with_esp_idf, cv.int_range(min=2)), + esp32=192, + esp32_s2=192, + esp32_s3=192, + esp32_p4=192, + esp32_c3=96, + esp32_c5=96, + esp32_c6=96, + esp32_h2=96, + ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), cv.Optional(CONF_FILTER_SYMBOLS): cv.All( - cv.only_with_esp_idf, cv.int_range(min=0) + cv.only_on_esp32, cv.int_range(min=0) ), cv.SplitDefault( CONF_RECEIVE_SYMBOLS, - esp32_idf=192, - ): cv.All(cv.only_with_esp_idf, cv.int_range(min=2)), + esp32=192, + ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), cv.Optional(CONF_USE_DMA): cv.All( esp32.only_on_variant( supported=[esp32.const.VARIANT_ESP32S3, esp32.const.VARIANT_ESP32P4] ), - cv.only_with_esp_idf, cv.boolean, ), } @@ -156,24 +140,15 @@ CONFIG_SCHEMA = remote_base.validate_triggers( async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) if CORE.is_esp32: - if esp32_rmt.use_new_rmt_driver(): - var = cg.new_Pvariable(config[CONF_ID], pin) - cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) - cg.add(var.set_receive_symbols(config[CONF_RECEIVE_SYMBOLS])) - if CONF_USE_DMA in config: - cg.add(var.set_with_dma(config[CONF_USE_DMA])) - if CONF_CLOCK_RESOLUTION in config: - cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) - if CONF_FILTER_SYMBOLS in config: - cg.add(var.set_filter_symbols(config[CONF_FILTER_SYMBOLS])) - else: - if (rmt_channel := config.get(CONF_RMT_CHANNEL, None)) is not None: - var = cg.new_Pvariable( - config[CONF_ID], pin, rmt_channel, config[CONF_MEMORY_BLOCKS] - ) - else: - var = cg.new_Pvariable(config[CONF_ID], pin, config[CONF_MEMORY_BLOCKS]) - cg.add(var.set_clock_divider(config[CONF_CLOCK_DIVIDER])) + var = cg.new_Pvariable(config[CONF_ID], pin) + cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) + cg.add(var.set_receive_symbols(config[CONF_RECEIVE_SYMBOLS])) + if CONF_USE_DMA in config: + cg.add(var.set_with_dma(config[CONF_USE_DMA])) + if CONF_CLOCK_RESOLUTION in config: + cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) + if CONF_FILTER_SYMBOLS in config: + cg.add(var.set_filter_symbols(config[CONF_FILTER_SYMBOLS])) else: var = cg.new_Pvariable(config[CONF_ID], pin) diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 8d19d5490f..9d844eee66 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -5,7 +5,7 @@ #include -#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5 +#if defined(USE_ESP32) #include #endif @@ -29,7 +29,7 @@ struct RemoteReceiverComponentStore { uint32_t filter_us{10}; ISRInternalGPIOPin pin; }; -#elif defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5 +#elif defined(USE_ESP32) struct RemoteReceiverComponentStore { /// Stores RMT symbols and rx done event data volatile uint8_t *buffer{nullptr}; @@ -55,21 +55,13 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, { public: -#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR < 5 - RemoteReceiverComponent(InternalGPIOPin *pin, uint8_t mem_block_num = 1) - : RemoteReceiverBase(pin), remote_base::RemoteRMTChannel(mem_block_num) {} - - RemoteReceiverComponent(InternalGPIOPin *pin, rmt_channel_t channel, uint8_t mem_block_num = 1) - : RemoteReceiverBase(pin), remote_base::RemoteRMTChannel(channel, mem_block_num) {} -#else RemoteReceiverComponent(InternalGPIOPin *pin) : RemoteReceiverBase(pin) {} -#endif void setup() override; void dump_config() override; void loop() override; float get_setup_priority() const override { return setup_priority::DATA; } -#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5 +#ifdef USE_ESP32 void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; } void set_receive_symbols(uint32_t receive_symbols) { this->receive_symbols_ = receive_symbols; } void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } @@ -80,21 +72,16 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, protected: #ifdef USE_ESP32 -#if ESP_IDF_VERSION_MAJOR >= 5 void decode_rmt_(rmt_symbol_word_t *item, size_t item_count); rmt_channel_handle_t channel_{NULL}; uint32_t filter_symbols_{0}; uint32_t receive_symbols_{0}; bool with_dma_{false}; -#else - void decode_rmt_(rmt_item32_t *item, size_t item_count); - RingbufHandle_t ringbuf_; -#endif esp_err_t error_code_{ESP_OK}; std::string error_string_{""}; #endif -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || (defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_ESP32) RemoteReceiverComponentStore store_; HighFrequencyLoopRequester high_freq_; #endif diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index b78928d857..3d6346baec 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -14,7 +14,6 @@ static const uint32_t RMT_CLK_FREQ = 32000000; static const uint32_t RMT_CLK_FREQ = 80000000; #endif -#if ESP_IDF_VERSION_MAJOR >= 5 static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *event, void *arg) { RemoteReceiverComponentStore *store = (RemoteReceiverComponentStore *) arg; rmt_rx_done_event_data_t *event_buffer = (rmt_rx_done_event_data_t *) (store->buffer + store->buffer_write); @@ -37,11 +36,9 @@ static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_r store->buffer_write = next_write; return false; } -#endif void RemoteReceiverComponent::setup() { ESP_LOGCONFIG(TAG, "Running setup"); -#if ESP_IDF_VERSION_MAJOR >= 5 rmt_rx_channel_config_t channel; memset(&channel, 0, sizeof(channel)); channel.clk_src = RMT_CLK_SRC_DEFAULT; @@ -105,62 +102,11 @@ void RemoteReceiverComponent::setup() { this->mark_failed(); return; } -#else - this->pin_->setup(); - rmt_config_t rmt{}; - this->config_rmt(rmt); - rmt.gpio_num = gpio_num_t(this->pin_->get_pin()); - rmt.rmt_mode = RMT_MODE_RX; - if (this->filter_us_ == 0) { - rmt.rx_config.filter_en = false; - } else { - rmt.rx_config.filter_en = true; - rmt.rx_config.filter_ticks_thresh = static_cast( - std::min(this->from_microseconds_(this->filter_us_) * this->clock_divider_, (uint32_t) 255)); - } - rmt.rx_config.idle_threshold = - static_cast(std::min(this->from_microseconds_(this->idle_us_), (uint32_t) 65535)); - - esp_err_t error = rmt_config(&rmt); - if (error != ESP_OK) { - this->error_code_ = error; - this->error_string_ = "in rmt_config"; - this->mark_failed(); - return; - } - - error = rmt_driver_install(this->channel_, this->buffer_size_, 0); - if (error != ESP_OK) { - this->error_code_ = error; - if (error == ESP_ERR_INVALID_STATE) { - this->error_string_ = str_sprintf("RMT channel %i is already in use by another component", this->channel_); - } else { - this->error_string_ = "in rmt_driver_install"; - } - this->mark_failed(); - return; - } - error = rmt_get_ringbuf_handle(this->channel_, &this->ringbuf_); - if (error != ESP_OK) { - this->error_code_ = error; - this->error_string_ = "in rmt_get_ringbuf_handle"; - this->mark_failed(); - return; - } - error = rmt_rx_start(this->channel_, true); - if (error != ESP_OK) { - this->error_code_ = error; - this->error_string_ = "in rmt_rx_start"; - this->mark_failed(); - return; - } -#endif } void RemoteReceiverComponent::dump_config() { ESP_LOGCONFIG(TAG, "Remote Receiver:"); LOG_PIN(" Pin: ", this->pin_); -#if ESP_IDF_VERSION_MAJOR >= 5 ESP_LOGCONFIG(TAG, " Clock resolution: %" PRIu32 " hz\n" " RMT symbols: %" PRIu32 "\n" @@ -172,22 +118,6 @@ void RemoteReceiverComponent::dump_config() { this->clock_resolution_, this->rmt_symbols_, this->filter_symbols_, this->receive_symbols_, this->tolerance_, (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->filter_us_, this->idle_us_); -#else - if (this->pin_->digital_read()) { - ESP_LOGW(TAG, "Remote Receiver Signal starts with a HIGH value. Usually this means you have to " - "invert the signal using 'inverted: True' in the pin schema!"); - } - ESP_LOGCONFIG(TAG, - " Channel: %d\n" - " RMT memory blocks: %d\n" - " Clock divider: %u\n" - " Tolerance: %" PRIu32 "%s\n" - " Filter out pulses shorter than: %" PRIu32 " us\n" - " Signal is done after %" PRIu32 " us of no changes", - this->channel_, this->mem_block_num_, this->clock_divider_, this->tolerance_, - (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->filter_us_, - this->idle_us_); -#endif if (this->is_failed()) { ESP_LOGE(TAG, "Configuring RMT driver failed: %s (%s)", esp_err_to_name(this->error_code_), this->error_string_.c_str()); @@ -195,7 +125,6 @@ void RemoteReceiverComponent::dump_config() { } void RemoteReceiverComponent::loop() { -#if ESP_IDF_VERSION_MAJOR >= 5 if (this->store_.error != ESP_OK) { ESP_LOGE(TAG, "Receive error"); this->error_code_ = this->store_.error; @@ -221,25 +150,9 @@ void RemoteReceiverComponent::loop() { this->call_listeners_dumpers_(); } } -#else - size_t len = 0; - auto *item = (rmt_item32_t *) xRingbufferReceive(this->ringbuf_, &len, 0); - if (item != nullptr) { - this->decode_rmt_(item, len / sizeof(rmt_item32_t)); - vRingbufferReturnItem(this->ringbuf_, item); - - if (!this->temp_.empty()) { - this->call_listeners_dumpers_(); - } - } -#endif } -#if ESP_IDF_VERSION_MAJOR >= 5 void RemoteReceiverComponent::decode_rmt_(rmt_symbol_word_t *item, size_t item_count) { -#else -void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t item_count) { -#endif bool prev_level = false; bool idle_level = false; uint32_t prev_length = 0; diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 4db24760d8..713cee0186 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -4,14 +4,12 @@ from esphome.components import esp32, esp32_rmt, remote_base import esphome.config_validation as cv from esphome.const import ( CONF_CARRIER_DUTY_PERCENT, - CONF_CLOCK_DIVIDER, CONF_CLOCK_RESOLUTION, CONF_ID, CONF_INVERTED, CONF_MODE, CONF_OPEN_DRAIN, CONF_PIN, - CONF_RMT_CHANNEL, CONF_RMT_SYMBOLS, CONF_USE_DMA, ) @@ -38,34 +36,26 @@ CONFIG_SCHEMA = cv.Schema( ), cv.Optional(CONF_CLOCK_RESOLUTION): cv.All( cv.only_on_esp32, - cv.only_with_esp_idf, esp32_rmt.validate_clock_resolution(), ), - cv.Optional(CONF_CLOCK_DIVIDER): cv.All( - cv.only_on_esp32, cv.only_with_arduino, cv.int_range(min=1, max=255) - ), - cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_with_esp_idf, cv.boolean), + cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_on_esp32, cv.boolean), cv.Optional(CONF_USE_DMA): cv.All( esp32.only_on_variant( supported=[esp32.const.VARIANT_ESP32S3, esp32.const.VARIANT_ESP32P4] ), - cv.only_with_esp_idf, cv.boolean, ), cv.SplitDefault( CONF_RMT_SYMBOLS, - esp32_idf=64, - esp32_s2_idf=64, - esp32_s3_idf=48, - esp32_p4_idf=48, - esp32_c3_idf=48, - esp32_c5_idf=48, - esp32_c6_idf=48, - esp32_h2_idf=48, - ): cv.All(cv.only_with_esp_idf, cv.int_range(min=2)), - cv.Optional(CONF_RMT_CHANNEL): cv.All( - cv.only_with_arduino, esp32_rmt.validate_rmt_channel(tx=True) - ), + esp32=64, + esp32_s2=64, + esp32_s3=48, + esp32_p4=48, + esp32_c3=48, + esp32_c5=48, + esp32_c6=48, + esp32_h2=48, + ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), } @@ -75,30 +65,21 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) if CORE.is_esp32: - if esp32_rmt.use_new_rmt_driver(): - var = cg.new_Pvariable(config[CONF_ID], pin) - cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) - if CONF_CLOCK_RESOLUTION in config: - cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) - if CONF_USE_DMA in config: - cg.add(var.set_with_dma(config[CONF_USE_DMA])) - if CONF_EOT_LEVEL in config: - cg.add(var.set_eot_level(config[CONF_EOT_LEVEL])) - else: - cg.add( - var.set_eot_level( - config[CONF_PIN][CONF_MODE][CONF_OPEN_DRAIN] - or config[CONF_PIN][CONF_INVERTED] - ) - ) + var = cg.new_Pvariable(config[CONF_ID], pin) + cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) + if CONF_CLOCK_RESOLUTION in config: + cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) + if CONF_USE_DMA in config: + cg.add(var.set_with_dma(config[CONF_USE_DMA])) + if CONF_EOT_LEVEL in config: + cg.add(var.set_eot_level(config[CONF_EOT_LEVEL])) else: - if (rmt_channel := config.get(CONF_RMT_CHANNEL, None)) is not None: - var = cg.new_Pvariable(config[CONF_ID], pin, rmt_channel) - else: - var = cg.new_Pvariable(config[CONF_ID], pin) - if CONF_CLOCK_DIVIDER in config: - cg.add(var.set_clock_divider(config[CONF_CLOCK_DIVIDER])) - + cg.add( + var.set_eot_level( + config[CONF_PIN][CONF_MODE][CONF_OPEN_DRAIN] + or config[CONF_PIN][CONF_INVERTED] + ) + ) else: var = cg.new_Pvariable(config[CONF_ID], pin) await cg.register_component(var, config) diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 0a8f354c72..f0dab2aaf8 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -5,7 +5,7 @@ #include -#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5 +#if defined(USE_ESP32) #include #endif @@ -20,15 +20,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, #endif { public: -#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR < 5 - RemoteTransmitterComponent(InternalGPIOPin *pin, uint8_t mem_block_num = 1) - : remote_base::RemoteTransmitterBase(pin), remote_base::RemoteRMTChannel(mem_block_num) {} - - RemoteTransmitterComponent(InternalGPIOPin *pin, rmt_channel_t channel, uint8_t mem_block_num = 1) - : remote_base::RemoteTransmitterBase(pin), remote_base::RemoteRMTChannel(channel, mem_block_num) {} -#else explicit RemoteTransmitterComponent(InternalGPIOPin *pin) : remote_base::RemoteTransmitterBase(pin) {} -#endif void setup() override; void dump_config() override; @@ -38,7 +30,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, void set_carrier_duty_percent(uint8_t carrier_duty_percent) { this->carrier_duty_percent_ = carrier_duty_percent; } -#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5 +#if defined(USE_ESP32) void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; } void digital_write(bool value); @@ -65,15 +57,11 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, uint32_t current_carrier_frequency_{38000}; bool initialized_{false}; -#if ESP_IDF_VERSION_MAJOR >= 5 std::vector rmt_temp_; bool with_dma_{false}; bool eot_level_{false}; rmt_channel_handle_t channel_{NULL}; rmt_encoder_handle_t encoder_{NULL}; -#else - std::vector rmt_temp_; -#endif esp_err_t error_code_{ESP_OK}; std::string error_string_{""}; bool inverted_{false}; diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index d51c45c607..411e380670 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -18,18 +18,10 @@ void RemoteTransmitterComponent::setup() { void RemoteTransmitterComponent::dump_config() { ESP_LOGCONFIG(TAG, "Remote Transmitter:"); -#if ESP_IDF_VERSION_MAJOR >= 5 ESP_LOGCONFIG(TAG, " Clock resolution: %" PRIu32 " hz\n" " RMT symbols: %" PRIu32, this->clock_resolution_, this->rmt_symbols_); -#else - ESP_LOGCONFIG(TAG, - " Channel: %d\n" - " RMT memory blocks: %d\n" - " Clock divider: %u", - this->channel_, this->mem_block_num_, this->clock_divider_); -#endif LOG_PIN(" Pin: ", this->pin_); if (this->current_carrier_frequency_ != 0 && this->carrier_duty_percent_ != 100) { @@ -42,7 +34,6 @@ void RemoteTransmitterComponent::dump_config() { } } -#if ESP_IDF_VERSION_MAJOR >= 5 void RemoteTransmitterComponent::digital_write(bool value) { rmt_symbol_word_t symbol = { .duration0 = 1, @@ -65,10 +56,8 @@ void RemoteTransmitterComponent::digital_write(bool value) { this->status_set_warning(); } } -#endif void RemoteTransmitterComponent::configure_rmt_() { -#if ESP_IDF_VERSION_MAJOR >= 5 esp_err_t error; if (!this->initialized_) { @@ -140,54 +129,6 @@ void RemoteTransmitterComponent::configure_rmt_() { this->mark_failed(); return; } -#else - rmt_config_t c{}; - - this->config_rmt(c); - c.rmt_mode = RMT_MODE_TX; - c.gpio_num = gpio_num_t(this->pin_->get_pin()); - c.tx_config.loop_en = false; - - if (this->current_carrier_frequency_ == 0 || this->carrier_duty_percent_ == 100) { - c.tx_config.carrier_en = false; - } else { - c.tx_config.carrier_en = true; - c.tx_config.carrier_freq_hz = this->current_carrier_frequency_; - c.tx_config.carrier_duty_percent = this->carrier_duty_percent_; - } - - c.tx_config.idle_output_en = true; - if (!this->inverted_) { - c.tx_config.carrier_level = RMT_CARRIER_LEVEL_HIGH; - c.tx_config.idle_level = RMT_IDLE_LEVEL_LOW; - } else { - c.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; - c.tx_config.idle_level = RMT_IDLE_LEVEL_HIGH; - } - - esp_err_t error = rmt_config(&c); - if (error != ESP_OK) { - this->error_code_ = error; - this->error_string_ = "in rmt_config"; - this->mark_failed(); - return; - } - - if (!this->initialized_) { - error = rmt_driver_install(this->channel_, 0, 0); - if (error != ESP_OK) { - this->error_code_ = error; - if (error == ESP_ERR_INVALID_STATE) { - this->error_string_ = str_sprintf("RMT channel %i is already in use by another component", this->channel_); - } else { - this->error_string_ = "in rmt_driver_install"; - } - this->mark_failed(); - return; - } - this->initialized_ = true; - } -#endif } void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { @@ -202,11 +143,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen this->rmt_temp_.clear(); this->rmt_temp_.reserve((this->temp_.get_data().size() + 1) / 2); uint32_t rmt_i = 0; -#if ESP_IDF_VERSION_MAJOR >= 5 rmt_symbol_word_t rmt_item; -#else - rmt_item32_t rmt_item; -#endif for (int32_t val : this->temp_.get_data()) { bool level = val >= 0; @@ -241,7 +178,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen return; } this->transmit_trigger_->trigger(); -#if ESP_IDF_VERSION_MAJOR >= 5 for (uint32_t i = 0; i < send_times; i++) { rmt_transmit_config_t config; memset(&config, 0, sizeof(config)); @@ -263,19 +199,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen if (i + 1 < send_times) delayMicroseconds(send_wait); } -#else - for (uint32_t i = 0; i < send_times; i++) { - esp_err_t error = rmt_write_items(this->channel_, this->rmt_temp_.data(), this->rmt_temp_.size(), true); - if (error != ESP_OK) { - ESP_LOGW(TAG, "rmt_write_items failed: %s", esp_err_to_name(error)); - this->status_set_warning(); - } else { - this->status_clear_warning(); - } - if (i + 1 < send_times) - delayMicroseconds(send_wait); - } -#endif this->complete_trigger_->trigger(); } diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index c7642d0637..d5839c1a2b 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -1,7 +1,7 @@ #include "sntp_component.h" #include "esphome/core/log.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esp_sntp.h" #elif USE_ESP8266 #include "sntp.h" @@ -16,7 +16,7 @@ static const char *const TAG = "sntp"; void SNTPComponent::setup() { ESP_LOGCONFIG(TAG, "Running setup"); -#if defined(USE_ESP_IDF) +#if defined(USE_ESP32) if (esp_sntp_enabled()) { esp_sntp_stop(); } @@ -46,7 +46,7 @@ void SNTPComponent::dump_config() { } } void SNTPComponent::update() { -#if !defined(USE_ESP_IDF) +#if !defined(USE_ESP32) // force resync if (sntp_enabled()) { sntp_stop(); diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index 2dc3acda77..d9e45242a8 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -9,7 +9,7 @@ #include #include #ifdef USE_WIFI_WPA2_EAP -#include +#include #endif #ifdef USE_WIFI_AP @@ -228,43 +228,43 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (ap.get_eap().has_value()) { // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. EAPAuth eap = ap.get_eap().value(); - err = esp_wifi_sta_wpa2_ent_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); + err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed! %d", err); + ESP_LOGV(TAG, "esp_eap_client_set_identity failed! %d", err); } int ca_cert_len = strlen(eap.ca_cert); int client_cert_len = strlen(eap.client_cert); int client_key_len = strlen(eap.client_key); if (ca_cert_len) { - err = esp_wifi_sta_wpa2_ent_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1); + err = esp_eap_client_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed! %d", err); + ESP_LOGV(TAG, "esp_eap_client_set_ca_cert failed! %d", err); } } // workout what type of EAP this is // validation is not required as the config tool has already validated it if (client_cert_len && client_key_len) { // if we have certs, this must be EAP-TLS - err = esp_wifi_sta_wpa2_ent_set_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1, - (uint8_t *) eap.client_key, client_key_len + 1, - (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); + err = esp_eap_client_set_certificate_and_key((uint8_t *) eap.client_cert, client_cert_len + 1, + (uint8_t *) eap.client_key, client_key_len + 1, + (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed! %d", err); + ESP_LOGV(TAG, "esp_eap_client_set_certificate_and_key failed! %d", err); } } else { // in the absence of certs, assume this is username/password based - err = esp_wifi_sta_wpa2_ent_set_username((uint8_t *) eap.username.c_str(), eap.username.length()); + err = esp_eap_client_set_username((uint8_t *) eap.username.c_str(), eap.username.length()); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed! %d", err); + ESP_LOGV(TAG, "esp_eap_client_set_username failed! %d", err); } - err = esp_wifi_sta_wpa2_ent_set_password((uint8_t *) eap.password.c_str(), eap.password.length()); + err = esp_eap_client_set_password((uint8_t *) eap.password.c_str(), eap.password.length()); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed! %d", err); + ESP_LOGV(TAG, "esp_eap_client_set_password failed! %d", err); } } - err = esp_wifi_sta_wpa2_ent_enable(); + err = esp_wifi_sta_enterprise_enable(); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", err); + ESP_LOGV(TAG, "esp_wifi_sta_enterprise_enable failed! %d", err); } } #endif // USE_WIFI_WPA2_EAP @@ -552,7 +552,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); #if USE_NETWORK_IPV6 - this->set_timeout(100, [] { WiFi.enableIpV6(); }); + this->set_timeout(100, [] { WiFi.enableIPv6(); }); #endif /* USE_NETWORK_IPV6 */ break; @@ -662,12 +662,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { -#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0) - const auto status = WiFiClass::status(); -#else const auto status = WiFi.status(); -#endif - if (status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST) { return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index f7a937c28d..a5d9f45e53 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -149,7 +149,7 @@ #define USE_WIFI_11KV_SUPPORT #ifdef USE_ARDUINO -#define USE_ARDUINO_VERSION_CODE VERSION_CODE(2, 0, 5) +#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 1, 3) #define USE_ETHERNET #endif diff --git a/platformio.ini b/platformio.ini index 96926eadd1..f67226d657 100644 --- a/platformio.ini +++ b/platformio.ini @@ -34,7 +34,6 @@ build_flags = [common] lib_deps = esphome/noise-c@0.1.4 ; api - makuna/NeoPixelBus@2.7.3 ; neopixelbus improv/Improv@1.2.4 ; improv_serial / esp32_improv bblanchon/ArduinoJson@6.18.5 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code @@ -101,6 +100,7 @@ lib_deps = ESP8266WiFi ; wifi (Arduino built-in) Update ; ota (Arduino built-in) ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp + makuna/NeoPixelBus@2.7.3 ; neopixelbus ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) @@ -118,23 +118,26 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino -platform = platformio/espressif32@5.4.0 +platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13/platform-espressif32.zip platform_packages = - platformio/framework-arduinoespressif32@~3.20005.0 + pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.1.3/esp32-3.1.3.zip framework = arduino lib_deps = ; order matters with lib-deps; some of the libs in common:arduino.lib_deps ; don't declare built-in libraries as dependencies, so they have to be declared first FS ; web_server_base (Arduino built-in) + Networking ; wifi,web_server_base,ethernet (Arduino built-in) WiFi ; wifi,web_server_base,ethernet (Arduino built-in) Update ; ota,web_server_base (Arduino built-in) ${common:arduino.lib_deps} ESP32Async/AsyncTCP@3.4.4 ; async_tcp - WiFiClientSecure ; http_request,nextion (Arduino built-in) + NetworkClientSecure ; http_request,nextion (Arduino built-in) HTTPClient ; http_request,nextion (Arduino built-in) ESPmDNS ; mdns (Arduino built-in) + ESP32 Async UDP ; captive_portal (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) + makuna/NeoPixelBus@2.8.0 ; neopixelbus esphome/ESP32-audioI2S@2.3.0 ; i2s_audio droscy/esp_wireguard@0.4.2 ; wireguard esphome/esp-audio-libs@1.1.4 ; audio diff --git a/tests/components/addressable_light/common-ard-esp32_rmt_led_strip.yaml b/tests/components/addressable_light/common-ard-esp32_rmt_led_strip.yaml index 9c5e63cdc6..a071f9df91 100644 --- a/tests/components/addressable_light/common-ard-esp32_rmt_led_strip.yaml +++ b/tests/components/addressable_light/common-ard-esp32_rmt_led_strip.yaml @@ -6,7 +6,6 @@ light: rgb_order: GRB num_leds: 256 pin: ${pin} - rmt_channel: 0 display: - platform: addressable_light diff --git a/tests/components/bluetooth_proxy/test.esp32-c3-ard.yaml b/tests/components/bluetooth_proxy/test.esp32-c3-ard.yaml deleted file mode 100644 index bf01b65b6f..0000000000 --- a/tests/components/bluetooth_proxy/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -<<: !include common.yaml - -esp32_ble_tracker: - max_connections: 3 - -bluetooth_proxy: - active: true - connection_slots: 2 diff --git a/tests/components/bluetooth_proxy/test.esp32-c3-idf.yaml b/tests/components/bluetooth_proxy/test.esp32-c6-idf.yaml similarity index 100% rename from tests/components/bluetooth_proxy/test.esp32-c3-idf.yaml rename to tests/components/bluetooth_proxy/test.esp32-c6-idf.yaml diff --git a/tests/components/bluetooth_proxy/test.esp32-ard.yaml b/tests/components/bluetooth_proxy/test.esp32-s3-ard.yaml similarity index 100% rename from tests/components/bluetooth_proxy/test.esp32-ard.yaml rename to tests/components/bluetooth_proxy/test.esp32-s3-ard.yaml diff --git a/tests/components/bluetooth_proxy/test.esp32-idf.yaml b/tests/components/bluetooth_proxy/test.esp32-s3-idf.yaml similarity index 100% rename from tests/components/bluetooth_proxy/test.esp32-idf.yaml rename to tests/components/bluetooth_proxy/test.esp32-s3-idf.yaml diff --git a/tests/components/e131/common-ard.yaml b/tests/components/e131/common-ard.yaml index 418453d6ef..8300dbb01b 100644 --- a/tests/components/e131/common-ard.yaml +++ b/tests/components/e131/common-ard.yaml @@ -8,7 +8,6 @@ light: rgb_order: GRB num_leds: 256 pin: ${pin} - rmt_channel: 0 effects: - e131: universe: 1 diff --git a/tests/components/esp32_camera_web_server/common.yaml b/tests/components/esp32_camera_web_server/common.yaml index 5edefdf0a8..fe2a6a2739 100644 --- a/tests/components/esp32_camera_web_server/common.yaml +++ b/tests/components/esp32_camera_web_server/common.yaml @@ -32,3 +32,7 @@ esp32_camera_web_server: mode: stream - port: 8081 mode: snapshot + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/esp32_rmt_led_strip/common-ard.yaml b/tests/components/esp32_rmt_led_strip/common-ard.yaml deleted file mode 100644 index 287690e86e..0000000000 --- a/tests/components/esp32_rmt_led_strip/common-ard.yaml +++ /dev/null @@ -1,18 +0,0 @@ -light: - - platform: esp32_rmt_led_strip - id: led_strip1 - pin: ${pin1} - num_leds: 60 - rmt_channel: 0 - rgb_order: GRB - chipset: ws2812 - - platform: esp32_rmt_led_strip - id: led_strip2 - pin: ${pin2} - num_leds: 60 - rmt_channel: 1 - rgb_order: RGB - bit0_high: 100us - bit0_low: 100us - bit1_high: 100us - bit1_low: 100us diff --git a/tests/components/esp32_rmt_led_strip/common-idf.yaml b/tests/components/esp32_rmt_led_strip/common.yaml similarity index 100% rename from tests/components/esp32_rmt_led_strip/common-idf.yaml rename to tests/components/esp32_rmt_led_strip/common.yaml diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-ard.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-ard.yaml index d5a9ec9435..0949b676d5 100644 --- a/tests/components/esp32_rmt_led_strip/test.esp32-ard.yaml +++ b/tests/components/esp32_rmt_led_strip/test.esp32-ard.yaml @@ -3,4 +3,4 @@ substitutions: pin2: GPIO14 packages: - common: !include common-ard.yaml + common: !include common.yaml diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-c3-ard.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-c3-ard.yaml index 2a3cdec60d..6cc0667e77 100644 --- a/tests/components/esp32_rmt_led_strip/test.esp32-c3-ard.yaml +++ b/tests/components/esp32_rmt_led_strip/test.esp32-c3-ard.yaml @@ -3,4 +3,4 @@ substitutions: pin2: GPIO4 packages: - common: !include common-ard.yaml + common: !include common.yaml diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-c3-idf.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-c3-idf.yaml index 8feded852c..6cc0667e77 100644 --- a/tests/components/esp32_rmt_led_strip/test.esp32-c3-idf.yaml +++ b/tests/components/esp32_rmt_led_strip/test.esp32-c3-idf.yaml @@ -3,4 +3,4 @@ substitutions: pin2: GPIO4 packages: - common: !include common-idf.yaml + common: !include common.yaml diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-idf.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-idf.yaml index bb26436e5b..0949b676d5 100644 --- a/tests/components/esp32_rmt_led_strip/test.esp32-idf.yaml +++ b/tests/components/esp32_rmt_led_strip/test.esp32-idf.yaml @@ -3,4 +3,4 @@ substitutions: pin2: GPIO14 packages: - common: !include common-idf.yaml + common: !include common.yaml diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-s3-idf.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-s3-idf.yaml index f64bb9d8a5..ad273903b2 100644 --- a/tests/components/esp32_rmt_led_strip/test.esp32-s3-idf.yaml +++ b/tests/components/esp32_rmt_led_strip/test.esp32-s3-idf.yaml @@ -3,7 +3,7 @@ substitutions: pin2: GPIO4 packages: - common: !include common-idf.yaml + common: !include common.yaml light: - id: !extend led_strip1 diff --git a/tests/components/internal_temperature/test.esp32-s3-ard.yaml b/tests/components/internal_temperature/test.esp32-s3-ard.yaml index bdd704756c..dade44d145 100644 --- a/tests/components/internal_temperature/test.esp32-s3-ard.yaml +++ b/tests/components/internal_temperature/test.esp32-s3-ard.yaml @@ -1,5 +1 @@ <<: !include common.yaml - -esp32: - framework: - version: 2.0.9 diff --git a/tests/components/partition/common-ard.yaml b/tests/components/partition/common-ard.yaml index 654eacf54f..b2ceadd6f7 100644 --- a/tests/components/partition/common-ard.yaml +++ b/tests/components/partition/common-ard.yaml @@ -5,7 +5,6 @@ light: chipset: ws2812 num_leds: 256 rgb_order: GRB - rmt_channel: 1 pin: ${pin} - platform: partition name: Partition Light diff --git a/tests/components/remote_receiver/esp32-common-ard.yaml b/tests/components/remote_receiver/esp32-common-ard.yaml deleted file mode 100644 index e331a35307..0000000000 --- a/tests/components/remote_receiver/esp32-common-ard.yaml +++ /dev/null @@ -1,14 +0,0 @@ -remote_receiver: - - id: rcvr - pin: ${pin} - rmt_channel: ${rmt_channel} - dump: all - tolerance: 25% - <<: !include common-actions.yaml - -binary_sensor: - - platform: remote_receiver - name: Panasonic Remote Input - panasonic: - address: 0x4004 - command: 0x100BCBD diff --git a/tests/components/remote_receiver/esp32-common-idf.yaml b/tests/components/remote_receiver/esp32-common.yaml similarity index 100% rename from tests/components/remote_receiver/esp32-common-idf.yaml rename to tests/components/remote_receiver/esp32-common.yaml diff --git a/tests/components/remote_receiver/test.esp32-ard.yaml b/tests/components/remote_receiver/test.esp32-ard.yaml index 5d29187206..10dd767598 100644 --- a/tests/components/remote_receiver/test.esp32-ard.yaml +++ b/tests/components/remote_receiver/test.esp32-ard.yaml @@ -1,6 +1,9 @@ substitutions: pin: GPIO2 - rmt_channel: "2" + clock_resolution: "2000000" + filter_symbols: "2" + receive_symbols: "4" + rmt_symbols: "64" packages: - common: !include esp32-common-ard.yaml + common: !include esp32-common.yaml diff --git a/tests/components/remote_receiver/test.esp32-c3-ard.yaml b/tests/components/remote_receiver/test.esp32-c3-ard.yaml index 5d29187206..10dd767598 100644 --- a/tests/components/remote_receiver/test.esp32-c3-ard.yaml +++ b/tests/components/remote_receiver/test.esp32-c3-ard.yaml @@ -1,6 +1,9 @@ substitutions: pin: GPIO2 - rmt_channel: "2" + clock_resolution: "2000000" + filter_symbols: "2" + receive_symbols: "4" + rmt_symbols: "64" packages: - common: !include esp32-common-ard.yaml + common: !include esp32-common.yaml diff --git a/tests/components/remote_receiver/test.esp32-c3-idf.yaml b/tests/components/remote_receiver/test.esp32-c3-idf.yaml index f017a2d807..10dd767598 100644 --- a/tests/components/remote_receiver/test.esp32-c3-idf.yaml +++ b/tests/components/remote_receiver/test.esp32-c3-idf.yaml @@ -6,4 +6,4 @@ substitutions: rmt_symbols: "64" packages: - common: !include esp32-common-idf.yaml + common: !include esp32-common.yaml diff --git a/tests/components/remote_receiver/test.esp32-idf.yaml b/tests/components/remote_receiver/test.esp32-idf.yaml index f017a2d807..10dd767598 100644 --- a/tests/components/remote_receiver/test.esp32-idf.yaml +++ b/tests/components/remote_receiver/test.esp32-idf.yaml @@ -6,4 +6,4 @@ substitutions: rmt_symbols: "64" packages: - common: !include esp32-common-idf.yaml + common: !include esp32-common.yaml diff --git a/tests/components/remote_receiver/test.esp32-s3-idf.yaml b/tests/components/remote_receiver/test.esp32-s3-idf.yaml index 74f49866cd..cdae8b1e4e 100644 --- a/tests/components/remote_receiver/test.esp32-s3-idf.yaml +++ b/tests/components/remote_receiver/test.esp32-s3-idf.yaml @@ -6,7 +6,7 @@ substitutions: rmt_symbols: "64" packages: - common: !include esp32-common-idf.yaml + common: !include esp32-common.yaml remote_receiver: - id: !extend rcvr diff --git a/tests/components/remote_transmitter/esp32-common-ard.yaml b/tests/components/remote_transmitter/esp32-common-ard.yaml deleted file mode 100644 index 420cea326d..0000000000 --- a/tests/components/remote_transmitter/esp32-common-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -remote_transmitter: - - id: xmitr - pin: ${pin} - rmt_channel: ${rmt_channel} - carrier_duty_percent: 50% - -packages: - buttons: !include common-buttons.yaml diff --git a/tests/components/remote_transmitter/esp32-common-idf.yaml b/tests/components/remote_transmitter/esp32-common.yaml similarity index 100% rename from tests/components/remote_transmitter/esp32-common-idf.yaml rename to tests/components/remote_transmitter/esp32-common.yaml diff --git a/tests/components/remote_transmitter/test.esp32-ard.yaml b/tests/components/remote_transmitter/test.esp32-ard.yaml index 5d29187206..0522f4d181 100644 --- a/tests/components/remote_transmitter/test.esp32-ard.yaml +++ b/tests/components/remote_transmitter/test.esp32-ard.yaml @@ -1,6 +1,7 @@ substitutions: pin: GPIO2 - rmt_channel: "2" + clock_resolution: "2000000" + rmt_symbols: "64" packages: - common: !include esp32-common-ard.yaml + common: !include esp32-common.yaml diff --git a/tests/components/remote_transmitter/test.esp32-c3-ard.yaml b/tests/components/remote_transmitter/test.esp32-c3-ard.yaml index c755b11563..0522f4d181 100644 --- a/tests/components/remote_transmitter/test.esp32-c3-ard.yaml +++ b/tests/components/remote_transmitter/test.esp32-c3-ard.yaml @@ -1,6 +1,7 @@ substitutions: pin: GPIO2 - rmt_channel: "1" + clock_resolution: "2000000" + rmt_symbols: "64" packages: - common: !include esp32-common-ard.yaml + common: !include esp32-common.yaml diff --git a/tests/components/remote_transmitter/test.esp32-c3-idf.yaml b/tests/components/remote_transmitter/test.esp32-c3-idf.yaml index cc1fe69b4d..0522f4d181 100644 --- a/tests/components/remote_transmitter/test.esp32-c3-idf.yaml +++ b/tests/components/remote_transmitter/test.esp32-c3-idf.yaml @@ -4,4 +4,4 @@ substitutions: rmt_symbols: "64" packages: - common: !include esp32-common-idf.yaml + common: !include esp32-common.yaml diff --git a/tests/components/remote_transmitter/test.esp32-idf.yaml b/tests/components/remote_transmitter/test.esp32-idf.yaml index cc1fe69b4d..0522f4d181 100644 --- a/tests/components/remote_transmitter/test.esp32-idf.yaml +++ b/tests/components/remote_transmitter/test.esp32-idf.yaml @@ -4,4 +4,4 @@ substitutions: rmt_symbols: "64" packages: - common: !include esp32-common-idf.yaml + common: !include esp32-common.yaml diff --git a/tests/components/remote_transmitter/test.esp32-s3-idf.yaml b/tests/components/remote_transmitter/test.esp32-s3-idf.yaml index d23463b531..fe4c46d9e7 100644 --- a/tests/components/remote_transmitter/test.esp32-s3-idf.yaml +++ b/tests/components/remote_transmitter/test.esp32-s3-idf.yaml @@ -4,7 +4,7 @@ substitutions: rmt_symbols: "64" packages: - common: !include esp32-common-idf.yaml + common: !include esp32-common.yaml remote_transmitter: - id: !extend xmitr diff --git a/tests/components/wled/test.esp32-ard.yaml b/tests/components/wled/test.esp32-ard.yaml index a24f28e154..156b31181e 100644 --- a/tests/components/wled/test.esp32-ard.yaml +++ b/tests/components/wled/test.esp32-ard.yaml @@ -12,6 +12,5 @@ light: rgb_order: GRB num_leds: 256 pin: 2 - rmt_channel: 0 effects: - wled: diff --git a/tests/components/wled/test.esp32-c3-ard.yaml b/tests/components/wled/test.esp32-c3-ard.yaml index a24f28e154..156b31181e 100644 --- a/tests/components/wled/test.esp32-c3-ard.yaml +++ b/tests/components/wled/test.esp32-c3-ard.yaml @@ -12,6 +12,5 @@ light: rgb_order: GRB num_leds: 256 pin: 2 - rmt_channel: 0 effects: - wled: From b7b1d17ecbf30762c9768e5e38fdfd81b8642b64 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Jun 2025 02:06:39 +0200 Subject: [PATCH 060/293] Remove empty generated protobuf methods (#9098) --- esphome/components/api/api_pb2.cpp | 28 -------------------------- esphome/components/api/api_pb2.h | 28 -------------------------- esphome/components/api/proto.h | 6 ++++-- script/api_protobuf/api_protobuf.py | 31 ++++++++++++++--------------- 4 files changed, 19 insertions(+), 74 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index bde1824d71..517b4d41b4 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -797,28 +797,18 @@ void ConnectResponse::dump_to(std::string &out) const { out.append("}"); } #endif -void DisconnectRequest::encode(ProtoWriteBuffer buffer) const {} -void DisconnectRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); } #endif -void DisconnectResponse::encode(ProtoWriteBuffer buffer) const {} -void DisconnectResponse::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); } #endif -void PingRequest::encode(ProtoWriteBuffer buffer) const {} -void PingRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); } #endif -void PingResponse::encode(ProtoWriteBuffer buffer) const {} -void PingResponse::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {}"); } #endif -void DeviceInfoRequest::encode(ProtoWriteBuffer buffer) const {} -void DeviceInfoRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); } #endif @@ -1039,18 +1029,12 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append("}"); } #endif -void ListEntitiesRequest::encode(ProtoWriteBuffer buffer) const {} -void ListEntitiesRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesRequest::dump_to(std::string &out) const { out.append("ListEntitiesRequest {}"); } #endif -void ListEntitiesDoneResponse::encode(ProtoWriteBuffer buffer) const {} -void ListEntitiesDoneResponse::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesDoneResponse::dump_to(std::string &out) const { out.append("ListEntitiesDoneResponse {}"); } #endif -void SubscribeStatesRequest::encode(ProtoWriteBuffer buffer) const {} -void SubscribeStatesRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeStatesRequest::dump_to(std::string &out) const { out.append("SubscribeStatesRequest {}"); } #endif @@ -3371,8 +3355,6 @@ void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { out.append("}"); } #endif -void SubscribeHomeassistantServicesRequest::encode(ProtoWriteBuffer buffer) const {} -void SubscribeHomeassistantServicesRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const { out.append("SubscribeHomeassistantServicesRequest {}"); @@ -3498,8 +3480,6 @@ void HomeassistantServiceResponse::dump_to(std::string &out) const { out.append("}"); } #endif -void SubscribeHomeAssistantStatesRequest::encode(ProtoWriteBuffer buffer) const {} -void SubscribeHomeAssistantStatesRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const { out.append("SubscribeHomeAssistantStatesRequest {}"); @@ -3603,8 +3583,6 @@ void HomeAssistantStateResponse::dump_to(std::string &out) const { out.append("}"); } #endif -void GetTimeRequest::encode(ProtoWriteBuffer buffer) const {} -void GetTimeRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); } #endif @@ -7499,8 +7477,6 @@ void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const { out.append("}"); } #endif -void SubscribeBluetoothConnectionsFreeRequest::encode(ProtoWriteBuffer buffer) const {} -void SubscribeBluetoothConnectionsFreeRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeBluetoothConnectionsFreeRequest::dump_to(std::string &out) const { out.append("SubscribeBluetoothConnectionsFreeRequest {}"); @@ -7784,8 +7760,6 @@ void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const { out.append("}"); } #endif -void UnsubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) const {} -void UnsubscribeBluetoothLEAdvertisementsRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void UnsubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { out.append("UnsubscribeBluetoothLEAdvertisementsRequest {}"); @@ -8451,8 +8425,6 @@ void VoiceAssistantWakeWord::dump_to(std::string &out) const { out.append("}"); } #endif -void VoiceAssistantConfigurationRequest::encode(ProtoWriteBuffer buffer) const {} -void VoiceAssistantConfigurationRequest::calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const { out.append("VoiceAssistantConfigurationRequest {}"); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 9d270bcdc1..7d92125290 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -357,8 +357,6 @@ class DisconnectRequest : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "disconnect_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -372,8 +370,6 @@ class DisconnectResponse : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "disconnect_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -387,8 +383,6 @@ class PingRequest : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "ping_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -402,8 +396,6 @@ class PingResponse : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "ping_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -417,8 +409,6 @@ class DeviceInfoRequest : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "device_info_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -468,8 +458,6 @@ class ListEntitiesRequest : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -483,8 +471,6 @@ class ListEntitiesDoneResponse : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_done_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -498,8 +484,6 @@ class SubscribeStatesRequest : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "subscribe_states_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1011,8 +995,6 @@ class SubscribeHomeassistantServicesRequest : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "subscribe_homeassistant_services_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1061,8 +1043,6 @@ class SubscribeHomeAssistantStatesRequest : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "subscribe_home_assistant_states_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1115,8 +1095,6 @@ class GetTimeRequest : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "get_time_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2117,8 +2095,6 @@ class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "subscribe_bluetooth_connections_free_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2244,8 +2220,6 @@ class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "unsubscribe_bluetooth_le_advertisements_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2512,8 +2486,6 @@ class VoiceAssistantConfigurationRequest : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "voice_assistant_configuration_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index eb0dbc151b..6ece509c8d 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -327,9 +327,11 @@ class ProtoWriteBuffer { class ProtoMessage { public: virtual ~ProtoMessage() = default; - virtual void encode(ProtoWriteBuffer buffer) const = 0; + // Default implementation for messages with no fields + virtual void encode(ProtoWriteBuffer buffer) const {} void decode(const uint8_t *buffer, size_t length); - virtual void calculate_size(uint32_t &total_size) const = 0; + // Default implementation for messages with no fields + virtual void calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP std::string dump() const; virtual void dump_to(std::string &out) const = 0; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 24b6bef843..5ac101c673 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -959,36 +959,35 @@ def build_message_type( prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;" protected_content.insert(0, prot) - o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{" + # Only generate encode method if there are fields to encode if encode: + o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{" if len(encode) == 1 and len(encode[0]) + len(o) + 3 < 120: o += f" {encode[0]} " else: o += "\n" o += indent("\n".join(encode)) + "\n" - o += "}\n" - cpp += o - prot = "void encode(ProtoWriteBuffer buffer) const override;" - public_content.append(prot) + o += "}\n" + cpp += o + prot = "void encode(ProtoWriteBuffer buffer) const override;" + public_content.append(prot) + # If no fields to encode, the default implementation in ProtoMessage will be used - # Add calculate_size method - o = f"void {desc.name}::calculate_size(uint32_t &total_size) const {{" - - # Add a check for empty/default objects to short-circuit the calculation - # Only add this optimization if we have fields to check + # Add calculate_size method only if there are fields if size_calc: + o = f"void {desc.name}::calculate_size(uint32_t &total_size) const {{" # For a single field, just inline it for simplicity if len(size_calc) == 1 and len(size_calc[0]) + len(o) + 3 < 120: o += f" {size_calc[0]} " else: - # For multiple fields, add a short-circuit check + # For multiple fields o += "\n" - # Performance optimization: add all the size calculations o += indent("\n".join(size_calc)) + "\n" - o += "}\n" - cpp += o - prot = "void calculate_size(uint32_t &total_size) const override;" - public_content.append(prot) + o += "}\n" + cpp += o + prot = "void calculate_size(uint32_t &total_size) const override;" + public_content.append(prot) + # If no fields to calculate size for, the default implementation in ProtoMessage will be used o = f"void {desc.name}::dump_to(std::string &out) const {{" if dump: From a08d021f77a9d076eebb24d28ae67c067bb44612 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Jun 2025 02:10:01 +0200 Subject: [PATCH 061/293] Reduce code duplication in auto-generated API protocol code (#9097) --- esphome/components/api/api_pb2_service.cpp | 442 +++++---------------- esphome/components/api/proto.h | 20 + script/api_protobuf/api_protobuf.py | 49 ++- 3 files changed, 151 insertions(+), 360 deletions(-) diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index dacb23c12b..03017fdfff 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -620,544 +620,300 @@ void APIServerConnection::on_ping_request(const PingRequest &msg) { } } void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - DeviceInfoResponse ret = this->device_info(msg); - if (!this->send_message(ret)) { - this->on_fatal_error(); + if (this->check_connection_setup_()) { + DeviceInfoResponse ret = this->device_info(msg); + if (!this->send_message(ret)) { + this->on_fatal_error(); + } } } void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->list_entities(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->list_entities(msg); } void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->subscribe_states(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->subscribe_states(msg); } void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->subscribe_logs(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->subscribe_logs(msg); } void APIServerConnection::on_subscribe_homeassistant_services_request( const SubscribeHomeassistantServicesRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->subscribe_homeassistant_services(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->subscribe_homeassistant_services(msg); } void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->subscribe_home_assistant_states(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->subscribe_home_assistant_states(msg); } void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - GetTimeResponse ret = this->get_time(msg); - if (!this->send_message(ret)) { - this->on_fatal_error(); + if (this->check_connection_setup_()) { + GetTimeResponse ret = this->get_time(msg); + if (!this->send_message(ret)) { + this->on_fatal_error(); + } } } void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->execute_service(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->execute_service(msg); } #ifdef USE_API_NOISE void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg); - if (!this->send_message(ret)) { - this->on_fatal_error(); + if (this->check_authenticated_()) { + NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg); + if (!this->send_message(ret)) { + this->on_fatal_error(); + } } } #endif #ifdef USE_BUTTON void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->button_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->button_command(msg); } #endif #ifdef USE_ESP32_CAMERA void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->camera_image(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->camera_image(msg); } #endif #ifdef USE_CLIMATE void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->climate_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->climate_command(msg); } #endif #ifdef USE_COVER void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->cover_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->cover_command(msg); } #endif #ifdef USE_DATETIME_DATE void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->date_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->date_command(msg); } #endif #ifdef USE_DATETIME_DATETIME void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->datetime_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->datetime_command(msg); } #endif #ifdef USE_FAN void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->fan_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->fan_command(msg); } #endif #ifdef USE_LIGHT void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->light_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->light_command(msg); } #endif #ifdef USE_LOCK void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->lock_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->lock_command(msg); } #endif #ifdef USE_MEDIA_PLAYER void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->media_player_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->media_player_command(msg); } #endif #ifdef USE_NUMBER void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->number_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->number_command(msg); } #endif #ifdef USE_SELECT void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->select_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->select_command(msg); } #endif #ifdef USE_SIREN void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->siren_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->siren_command(msg); } #endif #ifdef USE_SWITCH void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->switch_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->switch_command(msg); } #endif #ifdef USE_TEXT void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->text_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->text_command(msg); } #endif #ifdef USE_DATETIME_TIME void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->time_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->time_command(msg); } #endif #ifdef USE_UPDATE void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->update_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->update_command(msg); } #endif #ifdef USE_VALVE void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->valve_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->valve_command(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( const SubscribeBluetoothLEAdvertisementsRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->subscribe_bluetooth_le_advertisements(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->subscribe_bluetooth_le_advertisements(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->bluetooth_device_request(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->bluetooth_device_request(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->bluetooth_gatt_get_services(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->bluetooth_gatt_get_services(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->bluetooth_gatt_read(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->bluetooth_gatt_read(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->bluetooth_gatt_write(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->bluetooth_gatt_write(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->bluetooth_gatt_read_descriptor(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->bluetooth_gatt_read_descriptor(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->bluetooth_gatt_write_descriptor(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->bluetooth_gatt_write_descriptor(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->bluetooth_gatt_notify(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->bluetooth_gatt_notify(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_subscribe_bluetooth_connections_free_request( const SubscribeBluetoothConnectionsFreeRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg); - if (!this->send_message(ret)) { - this->on_fatal_error(); + if (this->check_authenticated_()) { + BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg); + if (!this->send_message(ret)) { + this->on_fatal_error(); + } } } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request( const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->unsubscribe_bluetooth_le_advertisements(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->unsubscribe_bluetooth_le_advertisements(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->bluetooth_scanner_set_mode(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->bluetooth_scanner_set_mode(msg); } #endif #ifdef USE_VOICE_ASSISTANT void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->subscribe_voice_assistant(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->subscribe_voice_assistant(msg); } #endif #ifdef USE_VOICE_ASSISTANT void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg); - if (!this->send_message(ret)) { - this->on_fatal_error(); + if (this->check_authenticated_()) { + VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg); + if (!this->send_message(ret)) { + this->on_fatal_error(); + } } } #endif #ifdef USE_VOICE_ASSISTANT void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->voice_assistant_set_configuration(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->voice_assistant_set_configuration(msg); } #endif #ifdef USE_ALARM_CONTROL_PANEL void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->alarm_control_panel_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->alarm_control_panel_command(msg); } #endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 6ece509c8d..e850236db6 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -379,6 +379,26 @@ class ProtoService { // Send the buffer return this->send_buffer(buffer, message_type); } + + // Authentication helper methods + bool check_connection_setup_() { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return false; + } + return true; + } + + bool check_authenticated_() { + if (!this->check_connection_setup_()) { + return false; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return false; + } + return true; + } }; } // namespace api diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 5ac101c673..bd1be66649 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1424,25 +1424,40 @@ def main() -> None: hpp_protected += f" void {on_func}(const {inp} &msg) override;\n" hpp += f" virtual {ret} {func}(const {inp} &msg) = 0;\n" cpp += f"void {class_name}::{on_func}(const {inp} &msg) {{\n" - body = "" - if needs_conn: - body += "if (!this->is_connection_setup()) {\n" - body += " this->on_no_setup_connection();\n" - body += " return;\n" - body += "}\n" - if needs_auth: - body += "if (!this->is_authenticated()) {\n" - body += " this->on_unauthenticated_access();\n" - body += " return;\n" - body += "}\n" - if is_void: - body += f"this->{func}(msg);\n" - else: - body += f"{ret} ret = this->{func}(msg);\n" - body += "if (!this->send_message(ret)) {\n" - body += " this->on_fatal_error();\n" + # Start with authentication/connection check if needed + if needs_auth or needs_conn: + # Determine which check to use + if needs_auth: + check_func = "this->check_authenticated_()" + else: + check_func = "this->check_connection_setup_()" + + body = f"if ({check_func}) {{\n" + + # Add the actual handler code, indented + handler_body = "" + if is_void: + handler_body = f"this->{func}(msg);\n" + else: + handler_body = f"{ret} ret = this->{func}(msg);\n" + handler_body += "if (!this->send_message(ret)) {\n" + handler_body += " this->on_fatal_error();\n" + handler_body += "}\n" + + body += indent(handler_body) + "\n" body += "}\n" + else: + # No auth check needed, just call the handler + body = "" + if is_void: + body += f"this->{func}(msg);\n" + else: + body += f"{ret} ret = this->{func}(msg);\n" + body += "if (!this->send_message(ret)) {\n" + body += " this->on_fatal_error();\n" + body += "}\n" + cpp += indent(body) + "\n" + "}\n" if ifdef is not None: From 95544e489dc59b1bb751b4c2d417d7ab41dfa35e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Jun 2025 02:10:50 +0200 Subject: [PATCH 062/293] Use smaller atomic types for ESP32 BLE Tracker ring buffer indices (#9106) --- .../components/esp32_ble_tracker/esp32_ble_tracker.cpp | 10 +++++----- .../components/esp32_ble_tracker/esp32_ble_tracker.h | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index c5906779f1..4785c29230 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -122,10 +122,10 @@ void ESP32BLETracker::loop() { // Consumer side: This runs in the main loop thread if (this->scanner_state_ == ScannerState::RUNNING) { // Load our own index with relaxed ordering (we're the only writer) - size_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed); + uint8_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed); // Load producer's index with acquire to see their latest writes - size_t write_idx = this->ring_write_index_.load(std::memory_order_acquire); + uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire); while (read_idx != write_idx) { // Process one result at a time directly from ring buffer @@ -409,11 +409,11 @@ void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) { // IMPORTANT: Only this thread writes to ring_write_index_ // Load our own index with relaxed ordering (we're the only writer) - size_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed); - size_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE; + uint8_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed); + uint8_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE; // Load consumer's index with acquire to see their latest updates - size_t read_idx = this->ring_read_index_.load(std::memory_order_acquire); + uint8_t read_idx = this->ring_read_index_.load(std::memory_order_acquire); // Check if buffer is full if (next_write_idx != read_idx) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 16a100fb47..490ed19645 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -289,9 +289,9 @@ class ESP32BLETracker : public Component, // Consumer: ESPHome main loop (loop() method) // This design ensures zero blocking in the BT callback and prevents scan result loss BLEScanResult *scan_ring_buffer_; - std::atomic ring_write_index_{0}; // Written only by BT callback (producer) - std::atomic ring_read_index_{0}; // Written only by main loop (consumer) - std::atomic scan_results_dropped_{0}; // Tracks buffer overflow events + std::atomic ring_write_index_{0}; // Written only by BT callback (producer) + std::atomic ring_read_index_{0}; // Written only by main loop (consumer) + std::atomic scan_results_dropped_{0}; // Tracks buffer overflow events esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; From 43c677ef37f84cc826191f16f148387979118497 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Jun 2025 02:12:14 +0200 Subject: [PATCH 063/293] Optimize API server performance by using cached loop time (#9104) --- esphome/components/api/api_server.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 6852afe937..740e4259b1 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -106,7 +106,7 @@ void APIServer::setup() { } #endif - this->last_connected_ = millis(); + this->last_connected_ = App.get_loop_component_start_time(); #ifdef USE_ESP32_CAMERA if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) { @@ -164,7 +164,7 @@ void APIServer::loop() { } if (this->reboot_timeout_ != 0) { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (!this->is_connected()) { if (now - this->last_connected_ > this->reboot_timeout_) { ESP_LOGE(TAG, "No client connected; rebooting"); From eeb0710ad461036521568eb3cc005ad5efa1c1b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Jun 2025 03:08:25 +0200 Subject: [PATCH 064/293] Optimize API component memory usage by reordering class members to reduce padding (#9111) --- esphome/components/api/api_connection.cpp | 45 +++++---- esphome/components/api/api_connection.h | 55 ++++++----- esphome/components/api/api_frame_helper.h | 106 ++++++++++++---------- esphome/components/api/api_server.h | 16 +++- 4 files changed, 125 insertions(+), 97 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 3e2b7c0154..ca5689bdf6 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -61,8 +61,8 @@ void APIConnection::start() { APIError err = this->helper_->init(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), - errno); + ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->get_client_combined_info().c_str(), + api_error_to_str(err), errno); return; } this->client_info_ = helper_->getpeername(); @@ -91,7 +91,7 @@ void APIConnection::loop() { // when network is disconnected force disconnect immediately // don't wait for timeout this->on_fatal_error(); - ESP_LOGW(TAG, "%s: Network unavailable; disconnecting", this->client_combined_info_.c_str()); + ESP_LOGW(TAG, "%s: Network unavailable; disconnecting", this->get_client_combined_info().c_str()); return; } if (this->next_close_) { @@ -104,7 +104,7 @@ void APIConnection::loop() { APIError err = this->helper_->loop(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(), + ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), errno); return; } @@ -118,12 +118,12 @@ void APIConnection::loop() { } else if (err != APIError::OK) { on_fatal_error(); if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str()); + ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); } else if (err == APIError::CONNECTION_CLOSED) { - ESP_LOGW(TAG, "%s: Connection closed", this->client_combined_info_.c_str()); + ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str()); } else { - ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), - errno); + ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(), + api_error_to_str(err), errno); } return; } else { @@ -157,7 +157,7 @@ void APIConnection::loop() { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) { on_fatal_error(); - ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->client_combined_info_.c_str()); + ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); } } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && now > this->next_ping_retry_) { ESP_LOGVV(TAG, "Sending keepalive PING"); @@ -166,7 +166,7 @@ void APIConnection::loop() { this->next_ping_retry_ = now + ping_retry_interval; this->ping_retries_++; std::string warn_str = str_sprintf("%s: Sending keepalive failed %u time(s);", - this->client_combined_info_.c_str(), this->ping_retries_); + this->get_client_combined_info().c_str(), this->ping_retries_); if (this->ping_retries_ >= max_ping_retries) { on_fatal_error(); ESP_LOGE(TAG, "%s disconnecting", warn_str.c_str()); @@ -233,7 +233,7 @@ DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) { // remote initiated disconnect_client // don't close yet, we still need to send the disconnect response // close will happen on next loop - ESP_LOGD(TAG, "%s disconnected", this->client_combined_info_.c_str()); + ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str()); this->next_close_ = true; DisconnectResponse resp; return resp; @@ -1544,8 +1544,7 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char HelloResponse APIConnection::hello(const HelloRequest &msg) { this->client_info_ = msg.client_info; this->client_peername_ = this->helper_->getpeername(); - this->client_combined_info_ = this->client_info_ + " (" + this->client_peername_ + ")"; - this->helper_->set_log_info(this->client_combined_info_); + this->helper_->set_log_info(this->get_client_combined_info()); this->client_api_version_major_ = msg.api_version_major; this->client_api_version_minor_ = msg.api_version_minor; ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.c_str(), @@ -1567,7 +1566,7 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { // bool invalid_password = 1; resp.invalid_password = !correct; if (correct) { - ESP_LOGD(TAG, "%s connected", this->client_combined_info_.c_str()); + ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); this->connection_state_ = ConnectionState::AUTHENTICATED; this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); #ifdef USE_HOMEASSISTANT_TIME @@ -1673,7 +1672,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { APIError err = this->helper_->loop(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(), + ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), errno); return false; } @@ -1695,10 +1694,10 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) if (err != APIError::OK) { on_fatal_error(); if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str()); + ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); } else { - ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), - errno); + ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(), + api_error_to_str(err), errno); } return false; } @@ -1707,11 +1706,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) } void APIConnection::on_unauthenticated_access() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s requested access without authentication", this->client_combined_info_.c_str()); + ESP_LOGD(TAG, "%s requested access without authentication", this->get_client_combined_info().c_str()); } void APIConnection::on_no_setup_connection() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s requested access without full connection", this->client_combined_info_.c_str()); + ESP_LOGD(TAG, "%s requested access without full connection", this->get_client_combined_info().c_str()); } void APIConnection::on_fatal_error() { this->helper_->close(); @@ -1860,10 +1859,10 @@ void APIConnection::process_batch_() { if (err != APIError::OK && err != APIError::WOULD_BLOCK) { on_fatal_error(); if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset during batch write", this->client_combined_info_.c_str()); + ESP_LOGW(TAG, "%s: Connection reset during batch write", this->get_client_combined_info().c_str()); } else { - ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), - errno); + ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), + api_error_to_str(err), errno); } } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 7cd41561d4..66b7ce38a7 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -275,7 +275,13 @@ class APIConnection : public APIServerConnection { bool try_to_clear_buffer(bool log_out_of_space); bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) override; - std::string get_client_combined_info() const { return this->client_combined_info_; } + std::string get_client_combined_info() const { + if (this->client_info_ == this->client_peername_) { + // Before Hello message, both are the same (just IP:port) + return this->client_info_; + } + return this->client_info_ + " (" + this->client_peername_ + ")"; + } // Buffer allocator methods for batch processing ProtoWriteBuffer allocate_single_message_buffer(uint16_t size); @@ -432,37 +438,44 @@ class APIConnection : public APIServerConnection { // Helper function to get estimated message size for buffer pre-allocation static uint16_t get_estimated_message_size(uint16_t message_type); - enum class ConnectionState { + // Pointers first (4 bytes each, naturally aligned) + std::unique_ptr helper_; + APIServer *parent_; + + // 4-byte aligned types + uint32_t last_traffic_; + uint32_t next_ping_retry_{0}; + int state_subs_at_ = -1; + + // Strings (12 bytes each on 32-bit) + std::string client_info_; + std::string client_peername_; + + // 2-byte aligned types + uint16_t client_api_version_major_{0}; + uint16_t client_api_version_minor_{0}; + + // Group all 1-byte types together to minimize padding + enum class ConnectionState : uint8_t { WAITING_FOR_HELLO, CONNECTED, AUTHENTICATED, } connection_state_{ConnectionState::WAITING_FOR_HELLO}; - + uint8_t log_subscription_{ESPHOME_LOG_LEVEL_NONE}; bool remove_{false}; - - std::unique_ptr helper_; - - std::string client_info_; - std::string client_peername_; - std::string client_combined_info_; - uint32_t client_api_version_major_{0}; - uint32_t client_api_version_minor_{0}; -#ifdef USE_ESP32_CAMERA - esp32_camera::CameraImageReader image_reader_; -#endif - bool state_subscription_{false}; - int log_subscription_{ESPHOME_LOG_LEVEL_NONE}; - uint32_t last_traffic_; - uint32_t next_ping_retry_{0}; - uint8_t ping_retries_{0}; bool sent_ping_{false}; bool service_call_subscription_{false}; bool next_close_ = false; - APIServer *parent_; + uint8_t ping_retries_{0}; + // 8 bytes used, no padding needed + + // Larger objects at the end InitialStateIterator initial_state_iterator_; ListEntitiesIterator list_entities_iterator_; - int state_subs_at_ = -1; +#ifdef USE_ESP32_CAMERA + esp32_camera::CameraImageReader image_reader_; +#endif // Function pointer type for message encoding using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index dc71a7ca17..7e90153091 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -125,38 +125,6 @@ class APIFrameHelper { const uint8_t *current_data() const { return data.data() + offset; } }; - // Queue of data buffers to be sent - std::deque tx_buf_; - - // Common state enum for all frame helpers - // Note: Not all states are used by all implementations - // - INITIALIZE: Used by both Noise and Plaintext - // - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol - // - DATA: Used by both Noise and Plaintext - // - CLOSED: Used by both Noise and Plaintext - // - FAILED: Used by both Noise and Plaintext - // - EXPLICIT_REJECT: Only used by Noise protocol - enum class State { - INITIALIZE = 1, - CLIENT_HELLO = 2, // Noise only - SERVER_HELLO = 3, // Noise only - HANDSHAKE = 4, // Noise only - DATA = 5, - CLOSED = 6, - FAILED = 7, - EXPLICIT_REJECT = 8, // Noise only - }; - - // Current state of the frame helper - State state_{State::INITIALIZE}; - - // Helper name for logging - std::string info_; - - // Socket for communication - socket::Socket *socket_{nullptr}; - std::unique_ptr socket_owned_; - // Common implementation for writing raw data to socket APIError write_raw_(const struct iovec *iov, int iovcnt); @@ -169,15 +137,41 @@ class APIFrameHelper { APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf, const std::string &info, StateEnum &state, StateEnum failed_state); + // Pointers first (4 bytes each) + socket::Socket *socket_{nullptr}; + std::unique_ptr socket_owned_; + + // Common state enum for all frame helpers + // Note: Not all states are used by all implementations + // - INITIALIZE: Used by both Noise and Plaintext + // - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol + // - DATA: Used by both Noise and Plaintext + // - CLOSED: Used by both Noise and Plaintext + // - FAILED: Used by both Noise and Plaintext + // - EXPLICIT_REJECT: Only used by Noise protocol + enum class State : uint8_t { + INITIALIZE = 1, + CLIENT_HELLO = 2, // Noise only + SERVER_HELLO = 3, // Noise only + HANDSHAKE = 4, // Noise only + DATA = 5, + CLOSED = 6, + FAILED = 7, + EXPLICIT_REJECT = 8, // Noise only + }; + + // Containers (size varies, but typically 12+ bytes on 32-bit) + std::deque tx_buf_; + std::string info_; + std::vector reusable_iovs_; + std::vector rx_buf_; + + // Group smaller types together + uint16_t rx_buf_len_ = 0; + State state_{State::INITIALIZE}; uint8_t frame_header_padding_{0}; uint8_t frame_footer_size_{0}; - - // Reusable IOV array for write_protobuf_packets to avoid repeated allocations - std::vector reusable_iovs_; - - // Receive buffer for reading frame data - std::vector rx_buf_; - uint16_t rx_buf_len_ = 0; + // 5 bytes total, 3 bytes padding // Common initialization for both plaintext and noise protocols APIError init_common_(); @@ -213,19 +207,28 @@ class APINoiseFrameHelper : public APIFrameHelper { APIError init_handshake_(); APIError check_handshake_finished_(); void send_explicit_handshake_reject_(const std::string &reason); + + // Pointers first (4 bytes each) + NoiseHandshakeState *handshake_{nullptr}; + NoiseCipherState *send_cipher_{nullptr}; + NoiseCipherState *recv_cipher_{nullptr}; + + // Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer) + std::shared_ptr ctx_; + + // Vector (12 bytes on 32-bit) + std::vector prologue_; + + // NoiseProtocolId (size depends on implementation) + NoiseProtocolId nid_; + + // Group small types together // Fixed-size header buffer for noise protocol: // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint) // Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase uint8_t rx_header_buf_[3]; uint8_t rx_header_buf_len_ = 0; - - std::vector prologue_; - - std::shared_ptr ctx_; - NoiseHandshakeState *handshake_{nullptr}; - NoiseCipherState *send_cipher_{nullptr}; - NoiseCipherState *recv_cipher_{nullptr}; - NoiseProtocolId nid_; + // 4 bytes total, no padding }; #endif // USE_API_NOISE @@ -252,6 +255,12 @@ class APIPlaintextFrameHelper : public APIFrameHelper { protected: APIError try_read_frame_(ParsedFrame *frame); + + // Group 2-byte aligned types + uint16_t rx_header_parsed_type_ = 0; + uint16_t rx_header_parsed_len_ = 0; + + // Group 1-byte types together // Fixed-size header buffer for plaintext protocol: // We now store the indicator byte + the two varints. // To match noise protocol's maximum message size (UINT16_MAX = 65535), we need: @@ -263,8 +272,7 @@ class APIPlaintextFrameHelper : public APIFrameHelper { uint8_t rx_header_buf_[6]; // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type) uint8_t rx_header_buf_pos_ = 0; bool rx_header_parsed_ = false; - uint16_t rx_header_parsed_type_ = 0; - uint16_t rx_header_parsed_len_ = 0; + // 8 bytes total, no padding needed }; #endif diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 971c192e4b..33412d8a68 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -142,19 +142,27 @@ class APIServer : public Component, public Controller { } protected: - bool shutting_down_ = false; + // Pointers and pointer-like types first (4 bytes each) std::unique_ptr socket_ = nullptr; - uint16_t port_{6053}; + Trigger *client_connected_trigger_ = new Trigger(); + Trigger *client_disconnected_trigger_ = new Trigger(); + + // 4-byte aligned types uint32_t reboot_timeout_{300000}; uint32_t batch_delay_{100}; uint32_t last_connected_{0}; + + // Vectors and strings (12 bytes each on 32-bit) std::vector> clients_; std::string password_; std::vector shared_write_buffer_; // Shared proto write buffer for all connections std::vector state_subs_; std::vector user_services_; - Trigger *client_connected_trigger_ = new Trigger(); - Trigger *client_disconnected_trigger_ = new Trigger(); + + // Group smaller types together + uint16_t port_{6053}; + bool shutting_down_ = false; + // 3 bytes used, 1 byte padding #ifdef USE_API_NOISE std::shared_ptr noise_ctx_ = std::make_shared(); From 2e11e66db473cf454b93f6dbd115d53e5e4f3b28 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Jun 2025 03:11:13 +0200 Subject: [PATCH 065/293] Optimize bluetooth_proxy memory usage on ESP32 (#9114) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../bluetooth_proxy/bluetooth_connection.h | 11 +++++-- .../bluetooth_proxy/bluetooth_proxy.h | 12 +++++-- .../esp32_ble_client/ble_client_base.h | 33 +++++++++++++------ .../esp32_ble_tracker/esp32_ble_tracker.h | 12 ++++--- 4 files changed, 49 insertions(+), 19 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index fd83f8dd00..73c034d93b 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -26,10 +26,17 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase { protected: friend class BluetoothProxy; - bool seen_mtu_or_services_{false}; - int16_t send_service_{-2}; + // Memory optimized layout for 32-bit systems + // Group 1: Pointers (4 bytes each, naturally aligned) BluetoothProxy *proxy_; + + // Group 2: 2-byte types + int16_t send_service_{-2}; // Needs to handle negative values and service count + + // Group 3: 1-byte types + bool seen_mtu_or_services_{false}; + // 1 byte used, 1 byte padding }; } // namespace bluetooth_proxy diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 16db0a0a11..f0632350e0 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -134,11 +134,17 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com BluetoothConnection *get_connection_(uint64_t address, bool reserve); - bool active_; - - std::vector connections_{}; + // Memory optimized layout for 32-bit systems + // Group 1: Pointers (4 bytes each, naturally aligned) api::APIConnection *api_connection_{nullptr}; + + // Group 2: Container types (typically 12 bytes on 32-bit) + std::vector connections_{}; + + // Group 3: 1-byte types grouped together + bool active_; bool raw_advertisements_{false}; + // 2 bytes used, 2 bytes padding }; extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 814a9664d9..bf3b589b1b 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -96,21 +96,34 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void set_state(espbt::ClientState st) override; protected: - int gattc_if_; - esp_bd_addr_t remote_bda_; - esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC}; - uint16_t conn_id_{UNSET_CONN_ID}; + // Memory optimized layout for 32-bit systems + // Group 1: 8-byte types uint64_t address_{0}; - bool auto_connect_{false}; + + // Group 2: Container types (grouped for memory optimization) std::string address_str_{}; - uint8_t connection_index_; - int16_t service_count_{0}; - uint16_t mtu_{23}; - bool paired_{false}; - espbt::ConnectionType connection_type_{espbt::ConnectionType::V1}; std::vector services_; + + // Group 3: 4-byte types + int gattc_if_; esp_gatt_status_t status_{ESP_GATT_OK}; + // Group 4: Arrays (6 bytes) + esp_bd_addr_t remote_bda_; + + // Group 5: 2-byte types + uint16_t conn_id_{UNSET_CONN_ID}; + uint16_t mtu_{23}; + + // Group 6: 1-byte types and small enums + esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC}; + espbt::ConnectionType connection_type_{espbt::ConnectionType::V1}; + uint8_t connection_index_; + uint8_t service_count_{0}; // ESP32 has max handles < 255, typical devices have < 50 services + bool auto_connect_{false}; + bool paired_{false}; + // 6 bytes used, 2 bytes padding + void log_event_(const char *name); }; diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 490ed19645..414c9f4b48 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -129,7 +129,7 @@ class ESPBTDeviceListener { ESP32BLETracker *parent_{nullptr}; }; -enum class ClientState { +enum class ClientState : uint8_t { // Connection is allocated INIT, // Client is disconnecting @@ -165,7 +165,7 @@ enum class ScannerState { STOPPED, }; -enum class ConnectionType { +enum class ConnectionType : uint8_t { // The default connection type, we hold all the services in ram // for the duration of the connection. V1, @@ -193,15 +193,19 @@ class ESPBTClient : public ESPBTDeviceListener { } } ClientState state() const { return state_; } - int app_id; + + // Memory optimized layout + uint8_t app_id; // App IDs are small integers assigned sequentially protected: + // Group 1: 1-byte types ClientState state_{ClientState::INIT}; // want_disconnect_ is set to true when a disconnect is requested // while the client is connecting. This is used to disconnect the // client as soon as we get the connection id (conn_id_) from the // ESP_GATTC_OPEN_EVT event. bool want_disconnect_{false}; + // 2 bytes used, 2 bytes padding }; class ESP32BLETracker : public Component, @@ -262,7 +266,7 @@ class ESP32BLETracker : public Component, /// Called to set the scanner state. Will also call callbacks to let listeners know when state is changed. void set_scanner_state_(ScannerState state); - int app_id_{0}; + uint8_t app_id_{0}; /// Vector of addresses that have already been printed in print_bt_device_info std::vector already_discovered_; From 8ba22183b962db2664112c772c3b5fbd88f0a7ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Jun 2025 03:30:41 +0200 Subject: [PATCH 066/293] Add enable_loop_soon_any_context() for thread and ISR-safe loop enabling (#9127) --- esphome/core/application.cpp | 94 ++++++++++++++++--- esphome/core/application.h | 3 + esphome/core/component.cpp | 23 ++++- esphome/core/component.h | 31 +++++- .../loop_test_component/__init__.py | 18 ++++ .../loop_test_isr_component.cpp | 80 ++++++++++++++++ .../loop_test_isr_component.h | 32 +++++++ .../fixtures/loop_disable_enable.yaml | 5 + tests/integration/test_loop_disable_enable.py | 59 +++++++++++- 9 files changed, 325 insertions(+), 20 deletions(-) create mode 100644 tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.cpp create mode 100644 tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.h diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 58df49f0f2..49c1e5fd61 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -97,6 +97,20 @@ void Application::loop() { // Feed WDT with time this->feed_wdt(last_op_end_time); + // Process any pending enable_loop requests from ISRs + // This must be done before marking in_loop_ = true to avoid race conditions + if (this->has_pending_enable_loop_requests_) { + // Clear flag BEFORE processing to avoid race condition + // If ISR sets it during processing, we'll catch it next loop iteration + // This is safe because: + // 1. Each component has its own pending_enable_loop_ flag that we check + // 2. If we can't process a component (wrong state), enable_pending_loops_() + // will set this flag back to true + // 3. Any new ISR requests during processing will set the flag again + this->has_pending_enable_loop_requests_ = false; + this->enable_pending_loops_(); + } + // Mark that we're in the loop for safe reentrant modifications this->in_loop_ = true; @@ -286,24 +300,82 @@ void Application::disable_component_loop_(Component *component) { } } +void Application::activate_looping_component_(uint16_t index) { + // Helper to move component from inactive to active section + if (index != this->looping_components_active_end_) { + std::swap(this->looping_components_[index], this->looping_components_[this->looping_components_active_end_]); + } + this->looping_components_active_end_++; +} + void Application::enable_component_loop_(Component *component) { - // This method must be reentrant - components can re-enable themselves during their own loop() call - // Single pass through all components to find and move if needed - // With typical 10-30 components, O(n) is faster than maintaining a map + // This method is only called when component state is LOOP_DONE, so we know + // the component must be in the inactive section (if it exists in looping_components_) + // Only search the inactive portion for better performance + // With typical 0-5 inactive components, O(k) is much faster than O(n) const uint16_t size = this->looping_components_.size(); - for (uint16_t i = 0; i < size; i++) { + for (uint16_t i = this->looping_components_active_end_; i < size; i++) { if (this->looping_components_[i] == component) { - if (i < this->looping_components_active_end_) { - return; // Already active - } // Found in inactive section - move to active - if (i != this->looping_components_active_end_) { - std::swap(this->looping_components_[i], this->looping_components_[this->looping_components_active_end_]); - } - this->looping_components_active_end_++; + this->activate_looping_component_(i); return; } } + // Component not found in looping_components_ - this is normal for components + // that don't have loop() or were not included in the partitioned vector +} + +void Application::enable_pending_loops_() { + // Process components that requested enable_loop from ISR context + // Only iterate through inactive looping_components_ (typically 0-5) instead of all components + // + // Race condition handling: + // 1. We check if component is already in LOOP state first - if so, just clear the flag + // This handles reentrancy where enable_loop() was called between ISR and processing + // 2. We only clear pending_enable_loop_ after checking state, preventing lost requests + // 3. If any components aren't in LOOP_DONE state, we set has_pending_enable_loop_requests_ + // back to true to ensure we check again next iteration + // 4. ISRs can safely set flags at any time - worst case is we process them next iteration + // 5. The global flag (has_pending_enable_loop_requests_) is cleared before this method, + // so any ISR that fires during processing will be caught in the next loop + const uint16_t size = this->looping_components_.size(); + bool has_pending = false; + + for (uint16_t i = this->looping_components_active_end_; i < size; i++) { + Component *component = this->looping_components_[i]; + if (!component->pending_enable_loop_) { + continue; // Skip components without pending requests + } + + // Check current state + uint8_t state = component->component_state_ & COMPONENT_STATE_MASK; + + // If already in LOOP state, nothing to do - clear flag and continue + if (state == COMPONENT_STATE_LOOP) { + component->pending_enable_loop_ = false; + continue; + } + + // If not in LOOP_DONE state, can't enable yet - keep flag set + if (state != COMPONENT_STATE_LOOP_DONE) { + has_pending = true; // Keep tracking this component + continue; // Keep the flag set - try again next iteration + } + + // Clear the pending flag and enable the loop + component->pending_enable_loop_ = false; + ESP_LOGD(TAG, "%s loop enabled from ISR", component->get_component_source()); + component->component_state_ &= ~COMPONENT_STATE_MASK; + component->component_state_ |= COMPONENT_STATE_LOOP; + + // Move to active section + this->activate_looping_component_(i); + } + + // If we couldn't process some requests, ensure we check again next iteration + if (has_pending) { + this->has_pending_enable_loop_requests_ = true; + } } #ifdef USE_SOCKET_SELECT_SUPPORT diff --git a/esphome/core/application.h b/esphome/core/application.h index ea298638d2..93d5a78958 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -577,6 +577,8 @@ class Application { // to ensure component state is properly updated along with the loop partition void disable_component_loop_(Component *component); void enable_component_loop_(Component *component); + void enable_pending_loops_(); + void activate_looping_component_(uint16_t index); void feed_wdt_arch_(); @@ -682,6 +684,7 @@ class Application { uint32_t loop_interval_{16}; size_t dump_config_at_{SIZE_MAX}; uint8_t app_state_{0}; + volatile bool has_pending_enable_loop_requests_{false}; Component *current_component_{nullptr}; uint32_t loop_component_start_time_{0}; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 3117f49ac1..625a7b2125 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -148,10 +148,12 @@ void Component::mark_failed() { App.disable_component_loop_(this); } void Component::disable_loop() { - ESP_LOGD(TAG, "%s loop disabled", this->get_component_source()); - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_LOOP_DONE; - App.disable_component_loop_(this); + if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { + ESP_LOGD(TAG, "%s loop disabled", this->get_component_source()); + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= COMPONENT_STATE_LOOP_DONE; + App.disable_component_loop_(this); + } } void Component::enable_loop() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { @@ -161,6 +163,19 @@ void Component::enable_loop() { App.enable_component_loop_(this); } } +void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { + // This method is thread and ISR-safe because: + // 1. Only performs simple assignments to volatile variables (atomic on all platforms) + // 2. No read-modify-write operations that could be interrupted + // 3. No memory allocation, object construction, or function calls + // 4. IRAM_ATTR ensures code is in IRAM, not flash (required for ISR execution) + // 5. Components are never destroyed, so no use-after-free concerns + // 6. App is guaranteed to be initialized before any ISR could fire + // 7. Multiple ISR/thread calls are safe - just sets the same flags to true + // 8. Race condition with main loop is handled by clearing flag before processing + this->pending_enable_loop_ = true; + App.has_pending_enable_loop_requests_ = true; +} void Component::reset_to_construction_state() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { ESP_LOGI(TAG, "Component %s is being reset to construction state", this->get_component_source()); diff --git a/esphome/core/component.h b/esphome/core/component.h index a37d64086a..7f2bdd8414 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -171,6 +171,27 @@ class Component { */ void enable_loop(); + /** Thread and ISR-safe version of enable_loop() that can be called from any context. + * + * This method defers the actual enable via enable_pending_loops_ to the main loop, + * making it safe to call from ISR handlers, timer callbacks, other threads, + * or any interrupt context. + * + * @note The actual loop enabling will happen on the next main loop iteration. + * @note Only one pending enable request is tracked per component. + * @note There is no disable_loop_soon_any_context() on purpose - it would race + * against enable calls and synchronization would get too complex + * to provide a safe version that would work for each component. + * + * Use disable_loop() from the main thread only. + * + * If you need to disable the loop from ISR, carefully implement + * it in the component itself, with an ISR safe approach, and call + * disable_loop() in its next ::loop() iteration. Implementations + * will need to carefully consider all possible race conditions. + */ + void enable_loop_soon_any_context(); + bool is_failed() const; bool is_ready() const; @@ -331,16 +352,18 @@ class Component { /// Cancel a defer callback using the specified name, name must not be empty. bool cancel_defer(const std::string &name); // NOLINT + // Ordered for optimal packing on 32-bit systems + float setup_priority_override_{NAN}; + const char *component_source_{nullptr}; + const char *error_message_{nullptr}; + uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s) /// State of this component - each bit has a purpose: /// Bits 0-1: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED) /// Bit 2: STATUS_LED_WARNING /// Bit 3: STATUS_LED_ERROR /// Bits 4-7: Unused - reserved for future expansion (50% of the bits are free) uint8_t component_state_{0x00}; - float setup_priority_override_{NAN}; - const char *component_source_{nullptr}; - uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s) - const char *error_message_{nullptr}; + volatile bool pending_enable_loop_{false}; ///< ISR-safe flag for enable_loop_soon_any_context }; /** This class simplifies creating components that periodically check a state. diff --git a/tests/integration/fixtures/external_components/loop_test_component/__init__.py b/tests/integration/fixtures/external_components/loop_test_component/__init__.py index c5eda67d1e..b66d4598f4 100644 --- a/tests/integration/fixtures/external_components/loop_test_component/__init__.py +++ b/tests/integration/fixtures/external_components/loop_test_component/__init__.py @@ -7,9 +7,13 @@ CODEOWNERS = ["@esphome/tests"] loop_test_component_ns = cg.esphome_ns.namespace("loop_test_component") LoopTestComponent = loop_test_component_ns.class_("LoopTestComponent", cg.Component) +LoopTestISRComponent = loop_test_component_ns.class_( + "LoopTestISRComponent", cg.Component +) CONF_DISABLE_AFTER = "disable_after" CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations" +CONF_ISR_COMPONENTS = "isr_components" COMPONENT_CONFIG_SCHEMA = cv.Schema( { @@ -20,10 +24,18 @@ COMPONENT_CONFIG_SCHEMA = cv.Schema( } ) +ISR_COMPONENT_CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(LoopTestISRComponent), + cv.Required(CONF_NAME): cv.string, + } +) + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(LoopTestComponent), cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA), + cv.Optional(CONF_ISR_COMPONENTS): cv.ensure_list(ISR_COMPONENT_CONFIG_SCHEMA), } ).extend(cv.COMPONENT_SCHEMA) @@ -76,3 +88,9 @@ async def to_code(config): comp_config[CONF_TEST_REDUNDANT_OPERATIONS] ) ) + + # Create ISR test components + for isr_config in config.get(CONF_ISR_COMPONENTS, []): + var = cg.new_Pvariable(isr_config[CONF_ID]) + await cg.register_component(var, isr_config) + cg.add(var.set_name(isr_config[CONF_NAME])) diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.cpp b/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.cpp new file mode 100644 index 0000000000..30afec0422 --- /dev/null +++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.cpp @@ -0,0 +1,80 @@ +#include "loop_test_isr_component.h" +#include "esphome/core/hal.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace loop_test_component { + +static const char *const ISR_TAG = "loop_test_isr_component"; + +void LoopTestISRComponent::setup() { + ESP_LOGI(ISR_TAG, "[%s] ISR component setup called", this->name_.c_str()); + this->last_check_time_ = millis(); +} + +void LoopTestISRComponent::loop() { + this->loop_count_++; + ESP_LOGI(ISR_TAG, "[%s] ISR component loop count: %d", this->name_.c_str(), this->loop_count_); + + // Disable after 5 loops + if (this->loop_count_ == 5) { + ESP_LOGI(ISR_TAG, "[%s] Disabling after 5 loops", this->name_.c_str()); + this->disable_loop(); + this->last_disable_time_ = millis(); + // Simulate ISR after disabling + this->set_timeout("simulate_isr_1", 50, [this]() { + ESP_LOGI(ISR_TAG, "[%s] Simulating ISR enable", this->name_.c_str()); + this->simulate_isr_enable(); + // Test reentrancy - call enable_loop() directly after ISR + // This simulates another thread calling enable_loop while processing ISR enables + this->set_timeout("test_reentrant", 10, [this]() { + ESP_LOGI(ISR_TAG, "[%s] Testing reentrancy - calling enable_loop() directly", this->name_.c_str()); + this->enable_loop(); + }); + }); + } + + // If we get here after being disabled, it means ISR re-enabled us + if (this->loop_count_ > 5 && this->loop_count_ < 10) { + ESP_LOGI(ISR_TAG, "[%s] Running after ISR re-enable! ISR was called %d times", this->name_.c_str(), + this->isr_call_count_); + } + + // Disable again after 10 loops to test multiple ISR enables + if (this->loop_count_ == 10) { + ESP_LOGI(ISR_TAG, "[%s] Disabling again after 10 loops", this->name_.c_str()); + this->disable_loop(); + this->last_disable_time_ = millis(); + + // Test pure ISR enable without any main loop enable + this->set_timeout("simulate_isr_2", 50, [this]() { + ESP_LOGI(ISR_TAG, "[%s] Testing pure ISR enable (no main loop enable)", this->name_.c_str()); + this->simulate_isr_enable(); + // DO NOT call enable_loop() - test that ISR alone works + }); + } + + // Log when we're running after second ISR enable + if (this->loop_count_ > 10) { + ESP_LOGI(ISR_TAG, "[%s] Running after pure ISR re-enable! ISR was called %d times total", this->name_.c_str(), + this->isr_call_count_); + } +} + +void IRAM_ATTR LoopTestISRComponent::simulate_isr_enable() { + // This simulates what would happen in a real ISR + // In a real scenario, this would be called from an actual interrupt handler + + this->isr_call_count_++; + + // Call enable_loop_soon_any_context multiple times to test that it's safe + this->enable_loop_soon_any_context(); + this->enable_loop_soon_any_context(); // Test multiple calls + this->enable_loop_soon_any_context(); // Should be idempotent + + // Note: In a real ISR, we cannot use ESP_LOG* macros as they're not ISR-safe + // For testing, we'll track the call count and log it from the main loop +} + +} // namespace loop_test_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.h b/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.h new file mode 100644 index 0000000000..20e11b5ecd --- /dev/null +++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.h @@ -0,0 +1,32 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace loop_test_component { + +class LoopTestISRComponent : public Component { + public: + void set_name(const std::string &name) { this->name_ = name; } + + void setup() override; + void loop() override; + + // Simulates an ISR calling enable_loop_soon_any_context + void simulate_isr_enable(); + + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + std::string name_; + int loop_count_{0}; + uint32_t last_disable_time_{0}; + uint32_t last_check_time_{0}; + bool isr_enable_pending_{false}; + int isr_call_count_{0}; +}; + +} // namespace loop_test_component +} // namespace esphome diff --git a/tests/integration/fixtures/loop_disable_enable.yaml b/tests/integration/fixtures/loop_disable_enable.yaml index 17010f7c34..f19d7f60ca 100644 --- a/tests/integration/fixtures/loop_disable_enable.yaml +++ b/tests/integration/fixtures/loop_disable_enable.yaml @@ -35,6 +35,11 @@ loop_test_component: test_redundant_operations: true disable_after: 10 + # ISR test component that uses enable_loop_soon_any_context + isr_components: + - id: isr_test + name: "isr_test" + # Interval to re-enable the self_disable_10 component after some time interval: - interval: 0.5s diff --git a/tests/integration/test_loop_disable_enable.py b/tests/integration/test_loop_disable_enable.py index 84301c25d8..d5f868aa93 100644 --- a/tests/integration/test_loop_disable_enable.py +++ b/tests/integration/test_loop_disable_enable.py @@ -41,17 +41,25 @@ async def test_loop_disable_enable( redundant_disable_tested = asyncio.Event() # Event fired when self_disable_10 component is re-enabled and runs again (count > 10) self_disable_10_re_enabled = asyncio.Event() + # Events for ISR component testing + isr_component_disabled = asyncio.Event() + isr_component_re_enabled = asyncio.Event() + isr_component_pure_re_enabled = asyncio.Event() # Track loop counts for components self_disable_10_counts: list[int] = [] normal_component_counts: list[int] = [] + isr_component_counts: list[int] = [] def on_log_line(line: str) -> None: """Process each log line from the process output.""" # Strip ANSI color codes clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) - if "loop_test_component" not in clean_line: + if ( + "loop_test_component" not in clean_line + and "loop_test_isr_component" not in clean_line + ): return log_messages.append(clean_line) @@ -92,6 +100,18 @@ async def test_loop_disable_enable( ): redundant_disable_tested.set() + # ISR component events + elif "[isr_test]" in clean_line: + if "ISR component loop count:" in clean_line: + count = int(clean_line.split("ISR component loop count: ")[1]) + isr_component_counts.append(count) + elif "Disabling after 5 loops" in clean_line: + isr_component_disabled.set() + elif "Running after ISR re-enable!" in clean_line: + isr_component_re_enabled.set() + elif "Running after pure ISR re-enable!" in clean_line: + isr_component_pure_re_enabled.set() + # Write, compile and run the ESPHome device with log callback async with ( run_compiled(yaml_config, line_callback=on_log_line), @@ -148,3 +168,40 @@ async def test_loop_disable_enable( assert later_self_disable_counts, ( "self_disable_10 was re-enabled but did not run additional times" ) + + # Test ISR component functionality + # Wait for ISR component to disable itself after 5 loops + try: + await asyncio.wait_for(isr_component_disabled.wait(), timeout=3.0) + except asyncio.TimeoutError: + pytest.fail("ISR component did not disable itself within 3 seconds") + + # Verify it ran exactly 5 times before disabling + first_run_counts = [c for c in isr_component_counts if c <= 5] + assert len(first_run_counts) == 5, ( + f"Expected 5 loops before disable, got {first_run_counts}" + ) + + # Wait for component to be re-enabled by periodic ISR simulation and run again + try: + await asyncio.wait_for(isr_component_re_enabled.wait(), timeout=2.0) + except asyncio.TimeoutError: + pytest.fail("ISR component was not re-enabled after ISR call") + + # Verify it's running again after ISR enable + count_after_isr = len(isr_component_counts) + assert count_after_isr > 5, ( + f"Component didn't run after ISR enable: got {count_after_isr} counts total" + ) + + # Wait for pure ISR enable (no main loop enable) to work + try: + await asyncio.wait_for(isr_component_pure_re_enabled.wait(), timeout=2.0) + except asyncio.TimeoutError: + pytest.fail("ISR component was not re-enabled by pure ISR call") + + # Verify it ran after pure ISR enable + final_count = len(isr_component_counts) + assert final_count > 10, ( + f"Component didn't run after pure ISR enable: got {final_count} counts total" + ) From 40a5638005fe3bcf00401c1e32ed8fa283e8a966 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Jun 2025 03:33:00 +0200 Subject: [PATCH 067/293] Optimize OTA loop to avoid unnecessary stack allocations (#9129) --- .../components/esphome/ota/ota_esphome.cpp | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 227cb676ff..4cc82b9094 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -26,19 +26,19 @@ void ESPHomeOTAComponent::setup() { ota::register_ota_platform(this); #endif - server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections - if (server_ == nullptr) { + this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections + if (this->server_ == nullptr) { ESP_LOGW(TAG, "Could not create socket"); this->mark_failed(); return; } int enable = 1; - int err = server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + int err = this->server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); if (err != 0) { ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); // we can still continue } - err = server_->setblocking(false); + err = this->server_->setblocking(false); if (err != 0) { ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); this->mark_failed(); @@ -54,14 +54,14 @@ void ESPHomeOTAComponent::setup() { return; } - err = server_->bind((struct sockaddr *) &server, sizeof(server)); + err = this->server_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); this->mark_failed(); return; } - err = server_->listen(4); + err = this->server_->listen(4); if (err != 0) { ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); this->mark_failed(); @@ -82,7 +82,14 @@ void ESPHomeOTAComponent::dump_config() { #endif } -void ESPHomeOTAComponent::loop() { this->handle_(); } +void ESPHomeOTAComponent::loop() { + // Skip handle_() call if no client connected and no incoming connections + // This optimization reduces idle loop overhead when OTA is not active + // Note: No need to check server_ for null as the component is marked failed in setup() if server_ creation fails + if (this->client_ != nullptr || this->server_->ready()) { + this->handle_(); + } +} static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01; @@ -101,23 +108,21 @@ void ESPHomeOTAComponent::handle_() { size_t size_acknowledged = 0; #endif - if (client_ == nullptr) { - // Check if the server socket is ready before accepting - if (this->server_->ready()) { - struct sockaddr_storage source_addr; - socklen_t addr_len = sizeof(source_addr); - client_ = server_->accept((struct sockaddr *) &source_addr, &addr_len); - } + if (this->client_ == nullptr) { + // We already checked server_->ready() in loop(), so we can accept directly + struct sockaddr_storage source_addr; + socklen_t addr_len = sizeof(source_addr); + this->client_ = this->server_->accept((struct sockaddr *) &source_addr, &addr_len); + if (this->client_ == nullptr) + return; } - if (client_ == nullptr) - return; int enable = 1; - int err = client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); + int err = this->client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); if (err != 0) { ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno); - client_->close(); - client_ = nullptr; + this->client_->close(); + this->client_ = nullptr; return; } From 2e9ac8945d62f6ae05970127a6ae50b3538c2382 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Thu, 19 Jun 2025 03:41:20 +0200 Subject: [PATCH 068/293] [nextion] Fix command spacing double timing and response blocking issues (#9134) --- esphome/components/nextion/nextion.cpp | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 3de32bfde9..24c31713bc 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -33,6 +33,7 @@ bool Nextion::send_command_(const std::string &command) { #ifdef USE_NEXTION_COMMAND_SPACING if (!this->ignore_is_setup_ && !this->command_pacer_.can_send()) { + ESP_LOGN(TAG, "Command spacing: delaying command '%s'", command.c_str()); return false; } #endif // USE_NEXTION_COMMAND_SPACING @@ -43,10 +44,6 @@ bool Nextion::send_command_(const std::string &command) { const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF}; this->write_array(to_send, sizeof(to_send)); -#ifdef USE_NEXTION_COMMAND_SPACING - this->command_pacer_.mark_sent(); -#endif // USE_NEXTION_COMMAND_SPACING - return true; } @@ -377,12 +374,6 @@ void Nextion::process_nextion_commands_() { size_t commands_processed = 0; #endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP -#ifdef USE_NEXTION_COMMAND_SPACING - if (!this->command_pacer_.can_send()) { - return; // Will try again in next loop iteration - } -#endif - size_t to_process_length = 0; std::string to_process; @@ -430,6 +421,7 @@ void Nextion::process_nextion_commands_() { } #ifdef USE_NEXTION_COMMAND_SPACING this->command_pacer_.mark_sent(); // Here is where we should mark the command as sent + ESP_LOGN(TAG, "Command spacing: marked command sent at %u ms", millis()); #endif break; case 0x02: // invalid Component ID or name was used From d527398dae111ad484dfda0b44f0a5d4042cb0fa Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Jun 2025 13:50:47 +1200 Subject: [PATCH 069/293] [i2c] Expose internal i2c bus port number (#9136) --- esphome/components/i2c/__init__.py | 6 ++++-- esphome/components/i2c/i2c_bus.h | 8 +++++++- esphome/components/i2c/i2c_bus_arduino.cpp | 5 +++-- esphome/components/i2c/i2c_bus_arduino.h | 9 ++++++--- esphome/components/i2c/i2c_bus_esp_idf.h | 8 +++++--- esphome/core/defines.h | 3 +++ 6 files changed, 28 insertions(+), 11 deletions(-) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index e47dec650d..d56bb2d07c 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -22,8 +22,9 @@ import esphome.final_validate as fv CODEOWNERS = ["@esphome/core"] i2c_ns = cg.esphome_ns.namespace("i2c") I2CBus = i2c_ns.class_("I2CBus") -ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", I2CBus, cg.Component) -IDFI2CBus = i2c_ns.class_("IDFI2CBus", I2CBus, cg.Component) +InternalI2CBus = i2c_ns.class_("InternalI2CBus", I2CBus) +ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", InternalI2CBus, cg.Component) +IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component) I2CDevice = i2c_ns.class_("I2CDevice") @@ -71,6 +72,7 @@ CONFIG_SCHEMA = cv.All( @coroutine_with_priority(1.0) async def to_code(config): cg.add_global(i2c_ns.using) + cg.add_define("USE_I2C") var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index fbfc88323e..5fa00b9d15 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -1,6 +1,6 @@ #pragma once -#include #include +#include #include #include @@ -108,5 +108,11 @@ class I2CBus { bool scan_{false}; ///< Should we scan ? Can be set in the yaml }; +class InternalI2CBus : public I2CBus { + /// @brief Returns the I2C port number. + /// @return the port number of the internal I2C bus + virtual int get_port() const = 0; +}; + } // namespace i2c } // namespace esphome diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index e9d8c2415c..a85df0a4cd 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -1,11 +1,11 @@ #ifdef USE_ARDUINO #include "i2c_bus_arduino.h" +#include +#include #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include -#include namespace esphome { namespace i2c { @@ -23,6 +23,7 @@ void ArduinoI2CBus::setup() { } else { wire_ = new TwoWire(next_bus_num); // NOLINT(cppcoreguidelines-owning-memory) } + this->port_ = next_bus_num; next_bus_num++; #elif defined(USE_ESP8266) wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory) diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index 6a670a2a05..7e6616cbce 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -2,9 +2,9 @@ #ifdef USE_ARDUINO -#include "i2c_bus.h" -#include "esphome/core/component.h" #include +#include "esphome/core/component.h" +#include "i2c_bus.h" namespace esphome { namespace i2c { @@ -15,7 +15,7 @@ enum RecoveryCode { RECOVERY_COMPLETED, }; -class ArduinoI2CBus : public I2CBus, public Component { +class ArduinoI2CBus : public InternalI2CBus, public Component { public: void setup() override; void dump_config() override; @@ -29,12 +29,15 @@ class ArduinoI2CBus : public I2CBus, public Component { void set_frequency(uint32_t frequency) { frequency_ = frequency; } void set_timeout(uint32_t timeout) { timeout_ = timeout; } + int get_port() const override { return this->port_; } + private: void recover_(); void set_pins_and_clock_(); RecoveryCode recovery_result_; protected: + int8_t port_{-1}; TwoWire *wire_; uint8_t sda_pin_; uint8_t scl_pin_; diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index afb4c2d22b..ee29578944 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -2,9 +2,9 @@ #ifdef USE_ESP_IDF -#include "i2c_bus.h" -#include "esphome/core/component.h" #include +#include "esphome/core/component.h" +#include "i2c_bus.h" namespace esphome { namespace i2c { @@ -15,7 +15,7 @@ enum RecoveryCode { RECOVERY_COMPLETED, }; -class IDFI2CBus : public I2CBus, public Component { +class IDFI2CBus : public InternalI2CBus, public Component { public: void setup() override; void dump_config() override; @@ -31,6 +31,8 @@ class IDFI2CBus : public I2CBus, public Component { void set_frequency(uint32_t frequency) { frequency_ = frequency; } void set_timeout(uint32_t timeout) { timeout_ = timeout; } + int get_port() const override { return static_cast(this->port_); } + private: void recover_(); RecoveryCode recovery_result_; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a5d9f45e53..657827c364 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -136,6 +136,7 @@ #define USE_ESP32_BLE_CLIENT #define USE_ESP32_BLE_SERVER #define USE_ESP32_CAMERA +#define USE_I2C #define USE_IMPROV #define USE_MICROPHONE #define USE_PSRAM @@ -179,6 +180,7 @@ #define USE_CAPTIVE_PORTAL #define USE_ESP8266_PREFERENCES_FLASH #define USE_HTTP_REQUEST_ESP8266_HTTPS +#define USE_I2C #define USE_SOCKET_IMPL_LWIP_TCP #define USE_SPI @@ -195,6 +197,7 @@ #ifdef USE_RP2040 #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 0) +#define USE_I2C #define USE_LOGGER_USB_CDC #define USE_SOCKET_IMPL_LWIP_TCP #define USE_SPI From 0ce3621ac0f6e646e853e1d18c7c7b8591f5e01e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Jun 2025 04:49:31 +0200 Subject: [PATCH 070/293] Disable Ethernet loop polling when connected and stable (#9102) --- esphome/components/ethernet/ethernet_component.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 0a6ba6470e..180a72ec7e 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -274,6 +274,9 @@ void EthernetComponent::loop() { ESP_LOGW(TAG, "Connection lost; reconnecting"); this->state_ = EthernetComponentState::CONNECTING; this->start_connect_(); + } else { + // When connected and stable, disable the loop to save CPU cycles + this->disable_loop(); } break; } @@ -397,11 +400,13 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base case ETHERNET_EVENT_START: event_name = "ETH started"; global_eth_component->started_ = true; + global_eth_component->enable_loop_soon_any_context(); break; case ETHERNET_EVENT_STOP: event_name = "ETH stopped"; global_eth_component->started_ = false; global_eth_component->connected_ = false; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes break; case ETHERNET_EVENT_CONNECTED: event_name = "ETH connected"; @@ -409,6 +414,7 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base case ETHERNET_EVENT_DISCONNECTED: event_name = "ETH disconnected"; global_eth_component->connected_ = false; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes break; default: return; @@ -425,8 +431,10 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b global_eth_component->got_ipv4_address_ = true; #if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes #else global_eth_component->connected_ = true; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes #endif /* USE_NETWORK_IPV6 */ } @@ -439,8 +447,10 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_ #if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) global_eth_component->connected_ = global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT); + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes #else global_eth_component->connected_ = global_eth_component->got_ipv4_address_; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes #endif } #endif /* USE_NETWORK_IPV6 */ @@ -620,6 +630,7 @@ bool EthernetComponent::powerdown() { } this->connected_ = false; this->started_ = false; + // No need to enable_loop() here as this is only called during shutdown/reboot if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) { ESP_LOGE(TAG, "Error powering down ethernet PHY"); return false; From a9e1a4cef35d9e6a3de6c6d069d37cd3ba2f1c37 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:53:54 +1200 Subject: [PATCH 071/293] Clean up RAMAllocators in audio related code (#9140) --- esphome/components/audio/audio_transfer_buffer.cpp | 4 ++-- .../components/i2s_audio/speaker/i2s_audio_speaker.cpp | 4 ++-- esphome/components/micro_wake_word/streaming_model.cpp | 4 ++-- esphome/components/voice_assistant/voice_assistant.cpp | 8 ++++---- esphome/core/ring_buffer.cpp | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/esphome/components/audio/audio_transfer_buffer.cpp b/esphome/components/audio/audio_transfer_buffer.cpp index 1566884c3d..790cd62db0 100644 --- a/esphome/components/audio/audio_transfer_buffer.cpp +++ b/esphome/components/audio/audio_transfer_buffer.cpp @@ -86,7 +86,7 @@ bool AudioTransferBuffer::reallocate(size_t new_buffer_size) { bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) { this->buffer_size_ = buffer_size; - RAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->buffer_ = allocator.allocate(this->buffer_size_); if (this->buffer_ == nullptr) { @@ -101,7 +101,7 @@ bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) { void AudioTransferBuffer::deallocate_buffer_() { if (this->buffer_ != nullptr) { - RAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; allocator.deallocate(this->buffer_, this->buffer_size_); this->buffer_ = nullptr; this->data_start_ = nullptr; diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 41da8a4642..1042a7ebee 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -484,7 +484,7 @@ bool I2SAudioSpeaker::send_esp_err_to_event_group_(esp_err_t err) { esp_err_t I2SAudioSpeaker::allocate_buffers_(size_t data_buffer_size, size_t ring_buffer_size) { if (this->data_buffer_ == nullptr) { // Allocate data buffer for temporarily storing audio from the ring buffer before writing to the I2S bus - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->data_buffer_ = allocator.allocate(data_buffer_size); } @@ -698,7 +698,7 @@ void I2SAudioSpeaker::delete_task_(size_t buffer_size) { this->audio_ring_buffer_.reset(); // Releases ownership of the shared_ptr if (this->data_buffer_ != nullptr) { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; allocator.deallocate(this->data_buffer_, buffer_size); this->data_buffer_ = nullptr; } diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp index 31341bba0d..2b073cce56 100644 --- a/esphome/components/micro_wake_word/streaming_model.cpp +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -27,7 +27,7 @@ void VADModel::log_model_config() { } bool StreamingModel::load_model_() { - RAMAllocator arena_allocator(RAMAllocator::ALLOW_FAILURE); + RAMAllocator arena_allocator; if (this->tensor_arena_ == nullptr) { this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_); @@ -96,7 +96,7 @@ bool StreamingModel::load_model_() { void StreamingModel::unload_model() { this->interpreter_.reset(); - RAMAllocator arena_allocator(RAMAllocator::ALLOW_FAILURE); + RAMAllocator arena_allocator; if (this->tensor_arena_ != nullptr) { arena_allocator.deallocate(this->tensor_arena_, this->tensor_arena_size_); diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index a692a7556e..366a020d1c 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -85,7 +85,7 @@ bool VoiceAssistant::start_udp_socket_() { bool VoiceAssistant::allocate_buffers_() { #ifdef USE_SPEAKER if ((this->speaker_ != nullptr) && (this->speaker_buffer_ == nullptr)) { - ExternalRAMAllocator speaker_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator speaker_allocator; this->speaker_buffer_ = speaker_allocator.allocate(SPEAKER_BUFFER_SIZE); if (this->speaker_buffer_ == nullptr) { ESP_LOGW(TAG, "Could not allocate speaker buffer"); @@ -103,7 +103,7 @@ bool VoiceAssistant::allocate_buffers_() { } if (this->send_buffer_ == nullptr) { - ExternalRAMAllocator send_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator send_allocator; this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE); if (send_buffer_ == nullptr) { ESP_LOGW(TAG, "Could not allocate send buffer"); @@ -136,7 +136,7 @@ void VoiceAssistant::clear_buffers_() { void VoiceAssistant::deallocate_buffers_() { if (this->send_buffer_ != nullptr) { - ExternalRAMAllocator send_deallocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator send_deallocator; send_deallocator.deallocate(this->send_buffer_, SEND_BUFFER_SIZE); this->send_buffer_ = nullptr; } @@ -147,7 +147,7 @@ void VoiceAssistant::deallocate_buffers_() { #ifdef USE_SPEAKER if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) { - ExternalRAMAllocator speaker_deallocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator speaker_deallocator; speaker_deallocator.deallocate(this->speaker_buffer_, SPEAKER_BUFFER_SIZE); this->speaker_buffer_ = nullptr; } diff --git a/esphome/core/ring_buffer.cpp b/esphome/core/ring_buffer.cpp index f779531263..b77a02b2a7 100644 --- a/esphome/core/ring_buffer.cpp +++ b/esphome/core/ring_buffer.cpp @@ -14,7 +14,7 @@ static const char *const TAG = "ring_buffer"; RingBuffer::~RingBuffer() { if (this->handle_ != nullptr) { vRingbufferDelete(this->handle_); - RAMAllocator allocator(RAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; allocator.deallocate(this->storage_, this->size_); } } @@ -24,7 +24,7 @@ std::unique_ptr RingBuffer::create(size_t len) { rb->size_ = len; - RAMAllocator allocator(RAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; rb->storage_ = allocator.allocate(rb->size_); if (rb->storage_ == nullptr) { return nullptr; From 9c90ca297a4b226ac7b27c6b09bae106c1629149 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Jun 2025 05:03:09 +0200 Subject: [PATCH 072/293] Fix missing BLE GAP events causing RSSI sensor and beacon failures (#9138) --- esphome/components/esp32_ble/ble.cpp | 97 ++++++++++++++---- esphome/components/esp32_ble/ble_event.h | 120 ++++++++++++++++++++--- 2 files changed, 188 insertions(+), 29 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 5a66f11d0f..cf63ad34d7 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -324,23 +324,69 @@ void ESP32BLE::loop() { } case BLEEvent::GAP: { esp_gap_ble_cb_event_t gap_event = ble_event->event_.gap.gap_event; - if (gap_event == ESP_GAP_BLE_SCAN_RESULT_EVT) { - // Use the new scan event handler - no memcpy! - for (auto *scan_handler : this->gap_scan_event_handlers_) { - scan_handler->gap_scan_event_handler(ble_event->scan_result()); - } - } else if (gap_event == ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT || - gap_event == ESP_GAP_BLE_SCAN_START_COMPLETE_EVT || - gap_event == ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT) { - // All three scan complete events have the same structure with just status - // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe - // This is verified at compile-time by static_assert checks in ble_event.h - // The struct already contains our copy of the status (copied in BLEEvent constructor) - ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler( - gap_event, reinterpret_cast(&ble_event->event_.gap.scan_complete)); - } + switch (gap_event) { + case ESP_GAP_BLE_SCAN_RESULT_EVT: + // Use the new scan event handler - no memcpy! + for (auto *scan_handler : this->gap_scan_event_handlers_) { + scan_handler->gap_scan_event_handler(ble_event->scan_result()); + } + break; + + // Scan complete events + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + // All three scan complete events have the same structure with just status + // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe + // This is verified at compile-time by static_assert checks in ble_event.h + // The struct already contains our copy of the status (copied in BLEEvent constructor) + ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler( + gap_event, reinterpret_cast(&ble_event->event_.gap.scan_complete)); + } + break; + + // Advertising complete events + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: + // All advertising complete events have the same structure with just status + ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler( + gap_event, reinterpret_cast(&ble_event->event_.gap.adv_complete)); + } + break; + + // RSSI complete event + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler( + gap_event, reinterpret_cast(&ble_event->event_.gap.read_rssi_complete)); + } + break; + + // Security events + case ESP_GAP_BLE_AUTH_CMPL_EVT: + case ESP_GAP_BLE_SEC_REQ_EVT: + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: + case ESP_GAP_BLE_PASSKEY_REQ_EVT: + case ESP_GAP_BLE_NC_REQ_EVT: + ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler( + gap_event, reinterpret_cast(&ble_event->event_.gap.security)); + } + break; + + default: + // Unknown/unhandled event + ESP_LOGW(TAG, "Unhandled GAP event type in loop: %d", gap_event); + break; } break; } @@ -399,11 +445,26 @@ template void enqueue_ble_event(esp_gattc_cb_event_t, esp_gatt_if_t, esp_ble_gat void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { - // Only queue the 4 GAP events we actually handle + // Queue GAP events that components need to handle + // Scanning events - used by esp32_ble_tracker case ESP_GAP_BLE_SCAN_RESULT_EVT: case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + // Advertising events - used by esp32_ble_beacon and esp32_ble server + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: + // Connection events - used by ble_client + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + // Security events - used by ble_client and bluetooth_proxy + case ESP_GAP_BLE_AUTH_CMPL_EVT: + case ESP_GAP_BLE_SEC_REQ_EVT: + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: + case ESP_GAP_BLE_PASSKEY_REQ_EVT: + case ESP_GAP_BLE_NC_REQ_EVT: enqueue_ble_event(event, param); return; diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index 30118d2afd..dd3ec3da42 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -24,16 +24,45 @@ static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param) == si "ESP-IDF scan_stop_cmpl structure has unexpected size"); // Verify the status field is at offset 0 (first member) -static_assert(offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl.status) == - offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl), +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl.status) == 0, "status must be first member of scan_param_cmpl"); -static_assert(offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl.status) == - offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl), +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl.status) == 0, "status must be first member of scan_start_cmpl"); -static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) == - offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl), +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) == 0, "status must be first member of scan_stop_cmpl"); +// Compile-time verification for advertising complete events +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_data_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF adv_data_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_rsp_data_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF scan_rsp_data_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_data_raw_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF adv_data_raw_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_start_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF adv_start_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_stop_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF adv_stop_cmpl structure has unexpected size"); + +// Verify the status field is at offset 0 for advertising events +static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_cmpl.status) == 0, + "status must be first member of adv_data_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_rsp_data_cmpl.status) == 0, + "status must be first member of scan_rsp_data_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_raw_cmpl.status) == 0, + "status must be first member of adv_data_raw_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, adv_start_cmpl.status) == 0, + "status must be first member of adv_start_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, adv_stop_cmpl.status) == 0, + "status must be first member of adv_stop_cmpl"); + +// Compile-time verification for RSSI complete event structure +static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.status) == 0, + "status must be first member of read_rssi_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.rssi) == sizeof(esp_bt_status_t), + "rssi must immediately follow status in read_rssi_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.remote_addr) == sizeof(esp_bt_status_t) + sizeof(int8_t), + "remote_addr must follow rssi in read_rssi_cmpl"); + // Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop(). // This class stores each event with minimal memory usage. // GAP events (99% of traffic) don't have the vector overhead. @@ -67,6 +96,17 @@ class BLEEvent { GATTS, }; + // Type definitions for cleaner method signatures + struct StatusOnlyData { + esp_bt_status_t status; + }; + + struct RSSICompleteData { + esp_bt_status_t status; + int8_t rssi; + esp_bd_addr_t remote_addr; + }; + // Constructor for GAP events - no external allocations needed BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { this->type_ = GAP; @@ -147,12 +187,21 @@ class BLEEvent { struct gap_event { esp_gap_ble_cb_event_t gap_event; union { - BLEScanResult scan_result; // 73 bytes + BLEScanResult scan_result; // 73 bytes - Used by: esp32_ble_tracker // This matches ESP-IDF's scan complete event structures // All three (scan_param_cmpl, scan_start_cmpl, scan_stop_cmpl) have identical layout - struct { - esp_bt_status_t status; - } scan_complete; // 1 byte + // Used by: esp32_ble_tracker + StatusOnlyData scan_complete; // 1 byte + // Advertising complete events all have same structure + // Used by: esp32_ble_beacon, esp32_ble server components + // ADV_DATA_SET, SCAN_RSP_DATA_SET, ADV_DATA_RAW_SET, ADV_START, ADV_STOP + StatusOnlyData adv_complete; // 1 byte + // RSSI complete event + // Used by: ble_client (ble_rssi_sensor component) + RSSICompleteData read_rssi_complete; // 8 bytes + // Security events - we store the full security union + // Used by: ble_client (automation), bluetooth_proxy, esp32_ble_client + esp_ble_sec_t security; // Variable size, but fits within scan_result size }; } gap; // 80 bytes total @@ -180,6 +229,9 @@ class BLEEvent { esp_gap_ble_cb_event_t gap_event_type() const { return event_.gap.gap_event; } const BLEScanResult &scan_result() const { return event_.gap.scan_result; } esp_bt_status_t scan_complete_status() const { return event_.gap.scan_complete.status; } + esp_bt_status_t adv_complete_status() const { return event_.gap.adv_complete.status; } + const RSSICompleteData &read_rssi_complete() const { return event_.gap.read_rssi_complete; } + const esp_ble_sec_t &security() const { return event_.gap.security; } private: // Initialize GAP event data @@ -215,8 +267,47 @@ class BLEEvent { this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status; break; + // Advertising complete events - all have same structure with just status + // Used by: esp32_ble_beacon, esp32_ble server components + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: + this->event_.gap.adv_complete.status = p->adv_data_cmpl.status; + break; + case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: + this->event_.gap.adv_complete.status = p->scan_rsp_data_cmpl.status; + break; + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: // Used by: esp32_ble_beacon + this->event_.gap.adv_complete.status = p->adv_data_raw_cmpl.status; + break; + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: // Used by: esp32_ble_beacon + this->event_.gap.adv_complete.status = p->adv_start_cmpl.status; + break; + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: // Used by: esp32_ble_beacon + this->event_.gap.adv_complete.status = p->adv_stop_cmpl.status; + break; + + // RSSI complete event + // Used by: ble_client (ble_rssi_sensor) + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + this->event_.gap.read_rssi_complete.status = p->read_rssi_cmpl.status; + this->event_.gap.read_rssi_complete.rssi = p->read_rssi_cmpl.rssi; + memcpy(this->event_.gap.read_rssi_complete.remote_addr, p->read_rssi_cmpl.remote_addr, sizeof(esp_bd_addr_t)); + break; + + // Security events - copy the entire security union + // Used by: ble_client, bluetooth_proxy, esp32_ble_client + case ESP_GAP_BLE_AUTH_CMPL_EVT: // Used by: bluetooth_proxy, esp32_ble_client + case ESP_GAP_BLE_SEC_REQ_EVT: // Used by: esp32_ble_client + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: // Used by: ble_client automation + case ESP_GAP_BLE_PASSKEY_REQ_EVT: // Used by: ble_client automation + case ESP_GAP_BLE_NC_REQ_EVT: // Used by: ble_client automation + memcpy(&this->event_.gap.security, &p->ble_security, sizeof(esp_ble_sec_t)); + break; + default: - // We only handle 4 GAP event types, others are dropped + // We only store data for GAP events that components currently use + // Unknown events still get queued and logged in ble.cpp:375 as + // "Unhandled GAP event type in loop" - this helps identify new events + // that components might need in the future break; } } @@ -295,6 +386,13 @@ class BLEEvent { } }; +// Verify the gap_event struct hasn't grown beyond expected size +// The gap member in the union should be 80 bytes (including the gap_event enum) +static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)) <= 80, "gap_event struct has grown beyond 80 bytes"); + +// Verify esp_ble_sec_t fits within our union +static_assert(sizeof(esp_ble_sec_t) <= 73, "esp_ble_sec_t is larger than BLEScanResult"); + // BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding) } // namespace esp32_ble From d4cb4ef99406faf60c1fbfee8d3d58acbef4aff4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Jun 2025 15:11:18 +1200 Subject: [PATCH 073/293] Clean up RAMAllocators in http_request code (#9143) --- esphome/components/http_request/http_request.h | 2 +- esphome/components/http_request/update/http_request_update.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index a67b04eadc..95515f731a 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -239,7 +239,7 @@ template class HttpRequestSendAction : public Action { std::string response_body; if (this->capture_response_.value(x...)) { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; uint8_t *buf = allocator.allocate(max_length); if (buf != nullptr) { size_t read_index = 0; diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index d683495ac6..828fb5bd8b 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -54,7 +54,7 @@ void HttpRequestUpdate::update_task(void *params) { UPDATE_RETURN; } - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; uint8_t *data = allocator.allocate(container->content_length); if (data == nullptr) { std::string msg = str_sprintf("Failed to allocate %d bytes for manifest", container->content_length); From 30bea20f7a1962bc74b37c805a124cd8a2bbd8fd Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Jun 2025 17:17:08 +1200 Subject: [PATCH 074/293] Clean up RAMAllocators in display related code (#9141) --- esphome/components/display/display_buffer.cpp | 2 +- esphome/components/font/font.h | 4 ++-- esphome/components/inkplate6/inkplate.cpp | 4 ++-- esphome/components/nextion/nextion.cpp | 12 ++++++------ .../components/nextion/nextion_upload_arduino.cpp | 8 ++++---- esphome/components/nextion/nextion_upload_idf.cpp | 14 +++++++------- .../waveshare_epaper/waveshare_epaper.cpp | 10 +++++----- 7 files changed, 27 insertions(+), 27 deletions(-) diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 3af1b63e01..0ecdccc38a 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -11,7 +11,7 @@ namespace display { static const char *const TAG = "display"; void DisplayBuffer::init_internal_(uint32_t buffer_length) { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->buffer_ = allocator.allocate(buffer_length); if (this->buffer_ == nullptr) { ESP_LOGE(TAG, "Could not allocate buffer for display!"); diff --git a/esphome/components/font/font.h b/esphome/components/font/font.h index 9ee23b3ec5..992c77cb9f 100644 --- a/esphome/components/font/font.h +++ b/esphome/components/font/font.h @@ -67,10 +67,10 @@ class Font inline int get_height() { return this->height_; } inline int get_bpp() { return this->bpp_; } - const std::vector> &get_glyphs() const { return glyphs_; } + const std::vector> &get_glyphs() const { return glyphs_; } protected: - std::vector> glyphs_; + std::vector> glyphs_; int baseline_; int height_; uint8_t bpp_; // bits per pixel diff --git a/esphome/components/inkplate6/inkplate.cpp b/esphome/components/inkplate6/inkplate.cpp index 247aa35ead..b3d0b87e83 100644 --- a/esphome/components/inkplate6/inkplate.cpp +++ b/esphome/components/inkplate6/inkplate.cpp @@ -57,8 +57,8 @@ void Inkplate6::setup() { * Allocate buffers. May be called after setup to re-initialise if e.g. greyscale is changed. */ void Inkplate6::initialize_() { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); - ExternalRAMAllocator allocator32(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; + RAMAllocator allocator32; uint32_t buffer_size = this->get_buffer_length_(); if (buffer_size == 0) return; diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 24c31713bc..e6fee10173 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -1,8 +1,8 @@ #include "nextion.h" -#include "esphome/core/util.h" -#include "esphome/core/log.h" -#include "esphome/core/application.h" #include +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" namespace esphome { namespace nextion { @@ -1003,7 +1003,7 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) { } #endif - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; nextion::NextionQueue *nextion_queue = allocator.allocate(1); if (nextion_queue == nullptr) { ESP_LOGW(TAG, "Queue alloc failed"); @@ -1159,7 +1159,7 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) { } #endif - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; nextion::NextionQueue *nextion_queue = allocator.allocate(1); if (nextion_queue == nullptr) { ESP_LOGW(TAG, "Queue alloc failed"); @@ -1191,7 +1191,7 @@ void Nextion::add_addt_command_to_queue(NextionComponentBase *component) { if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) return; - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; nextion::NextionQueue *nextion_queue = allocator.allocate(1); if (nextion_queue == nullptr) { ESP_LOGW(TAG, "Queue alloc failed"); diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index 6652e70172..aa7350bb57 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -3,12 +3,12 @@ #ifdef USE_NEXTION_TFT_UPLOAD #ifdef USE_ARDUINO +#include +#include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" -#include "esphome/core/util.h" #include "esphome/core/log.h" -#include "esphome/components/network/util.h" -#include +#include "esphome/core/util.h" #ifdef USE_ESP32 #include @@ -52,7 +52,7 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { } // Allocate the buffer dynamically - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; uint8_t *buffer = allocator.allocate(4096); if (!buffer) { ESP_LOGE(TAG, "Buffer alloc failed"); diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp index fc98056bc3..43b80f7761 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -3,14 +3,14 @@ #ifdef USE_NEXTION_TFT_UPLOAD #ifdef USE_ESP_IDF -#include "esphome/core/application.h" -#include "esphome/core/defines.h" -#include "esphome/core/util.h" -#include "esphome/core/log.h" -#include "esphome/components/network/util.h" -#include #include #include +#include +#include "esphome/components/network/util.h" +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" namespace esphome { namespace nextion { @@ -51,7 +51,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r } // Allocate the buffer dynamically - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; uint8_t *buffer = allocator.allocate(4096); if (!buffer) { ESP_LOGE(TAG, "Buffer alloc failed"); diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 084747c09e..575234e780 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -1,9 +1,9 @@ #include "waveshare_epaper.h" +#include +#include #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include -#include namespace esphome { namespace waveshare_epaper { @@ -185,7 +185,7 @@ void WaveshareEPaper7C::setup() { this->initialize(); } void WaveshareEPaper7C::init_internal_7c_(uint32_t buffer_length) { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; uint32_t small_buffer_length = buffer_length / NUM_BUFFERS; for (int i = 0; i < NUM_BUFFERS; i++) { @@ -2054,7 +2054,7 @@ void GDEW029T5::initialize() { this->deep_sleep_between_updates_ = true; // old buffer for partial update - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->old_buffer_ = allocator.allocate(this->get_buffer_length_()); if (this->old_buffer_ == nullptr) { ESP_LOGE(TAG, "Could not allocate old buffer for display!"); @@ -2199,7 +2199,7 @@ void GDEW029T5::dump_config() { void GDEW0154M09::initialize() { this->init_internal_(); - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->lastbuff_ = allocator.allocate(this->get_buffer_length_()); if (this->lastbuff_ != nullptr) { memset(this->lastbuff_, 0xff, sizeof(uint8_t) * this->get_buffer_length_()); From 2c17b2bacc2b4540f51ca883cbaebc28b38f20c9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Jun 2025 17:44:33 +1200 Subject: [PATCH 075/293] [i2c] Make ``get_port()`` public (#9146) --- esphome/components/i2c/i2c_bus.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index 5fa00b9d15..5c1e15d814 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -109,6 +109,7 @@ class I2CBus { }; class InternalI2CBus : public I2CBus { + public: /// @brief Returns the I2C port number. /// @return the port number of the internal I2C bus virtual int get_port() const = 0; From 4d0f8528d2e78b2860a43a0bc4fb77bd7d755b73 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:31:19 +1200 Subject: [PATCH 076/293] [esp32_camera] Allow sharing i2c bus (#9137) Co-authored-by: Keith Burzinski --- esphome/components/esp32_camera/__init__.py | 195 ++++++++++-------- .../components/esp32_camera/esp32_camera.cpp | 17 +- .../components/esp32_camera/esp32_camera.h | 16 +- 3 files changed, 134 insertions(+), 94 deletions(-) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index b4038c1841..05522265ae 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -1,5 +1,6 @@ from esphome import automation, pins import esphome.codegen as cg +from esphome.components import i2c from esphome.components.esp32 import add_idf_component import esphome.config_validation as cv from esphome.const import ( @@ -7,6 +8,7 @@ from esphome.const import ( CONF_CONTRAST, CONF_DATA_PINS, CONF_FREQUENCY, + CONF_I2C_ID, CONF_ID, CONF_PIN, CONF_RESET_PIN, @@ -149,93 +151,104 @@ CONF_ON_IMAGE = "on_image" camera_range_param = cv.int_range(min=-2, max=2) -CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(ESP32Camera), - # pin assignment - cv.Required(CONF_DATA_PINS): cv.All( - [pins.internal_gpio_input_pin_number], cv.Length(min=8, max=8) - ), - cv.Required(CONF_VSYNC_PIN): pins.internal_gpio_input_pin_number, - cv.Required(CONF_HREF_PIN): pins.internal_gpio_input_pin_number, - cv.Required(CONF_PIXEL_CLOCK_PIN): pins.internal_gpio_input_pin_number, - cv.Required(CONF_EXTERNAL_CLOCK): cv.Schema( - { - cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number, - cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All( - cv.frequency, cv.Range(min=8e6, max=20e6) - ), - } - ), - cv.Required(CONF_I2C_PINS): cv.Schema( - { - cv.Required(CONF_SDA): pins.internal_gpio_output_pin_number, - cv.Required(CONF_SCL): pins.internal_gpio_output_pin_number, - } - ), - cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_POWER_DOWN_PIN): pins.internal_gpio_output_pin_number, - # image - cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum( - FRAME_SIZES, upper=True - ), - cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63), - cv.Optional(CONF_CONTRAST, default=0): camera_range_param, - cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, - cv.Optional(CONF_SATURATION, default=0): camera_range_param, - cv.Optional(CONF_VERTICAL_FLIP, default=True): cv.boolean, - cv.Optional(CONF_HORIZONTAL_MIRROR, default=True): cv.boolean, - cv.Optional(CONF_SPECIAL_EFFECT, default="NONE"): cv.enum( - ENUM_SPECIAL_EFFECT, upper=True - ), - # exposure - cv.Optional(CONF_AGC_MODE, default="AUTO"): cv.enum( - ENUM_GAIN_CONTROL_MODE, upper=True - ), - cv.Optional(CONF_AEC2, default=False): cv.boolean, - cv.Optional(CONF_AE_LEVEL, default=0): camera_range_param, - cv.Optional(CONF_AEC_VALUE, default=300): cv.int_range(min=0, max=1200), - # gains - cv.Optional(CONF_AEC_MODE, default="AUTO"): cv.enum( - ENUM_GAIN_CONTROL_MODE, upper=True - ), - cv.Optional(CONF_AGC_VALUE, default=0): cv.int_range(min=0, max=30), - cv.Optional(CONF_AGC_GAIN_CEILING, default="2X"): cv.enum( - ENUM_GAIN_CEILING, upper=True - ), - # white balance - cv.Optional(CONF_WB_MODE, default="AUTO"): cv.enum(ENUM_WB_MODE, upper=True), - # test pattern - cv.Optional(CONF_TEST_PATTERN, default=False): cv.boolean, - # framerates - cv.Optional(CONF_MAX_FRAMERATE, default="10 fps"): cv.All( - cv.framerate, cv.Range(min=0, min_included=False, max=60) - ), - cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All( - cv.framerate, cv.Range(min=0, max=1) - ), - cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), - cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ESP32CameraStreamStartTrigger - ), - } - ), - cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ESP32CameraStreamStopTrigger - ), - } - ), - cv.Optional(CONF_ON_IMAGE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32CameraImageTrigger), - } - ), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.All( + cv.ENTITY_BASE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ESP32Camera), + # pin assignment + cv.Required(CONF_DATA_PINS): cv.All( + [pins.internal_gpio_input_pin_number], cv.Length(min=8, max=8) + ), + cv.Required(CONF_VSYNC_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_HREF_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_PIXEL_CLOCK_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_EXTERNAL_CLOCK): cv.Schema( + { + cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number, + cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All( + cv.frequency, cv.Range(min=8e6, max=20e6) + ), + } + ), + cv.Optional(CONF_I2C_PINS): cv.Schema( + { + cv.Required(CONF_SDA): pins.internal_gpio_output_pin_number, + cv.Required(CONF_SCL): pins.internal_gpio_output_pin_number, + } + ), + cv.Optional(CONF_I2C_ID): cv.Any( + cv.use_id(i2c.InternalI2CBus), + msg="I2C bus must be an internal ESP32 I2C bus", + ), + cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_POWER_DOWN_PIN): pins.internal_gpio_output_pin_number, + # image + cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum( + FRAME_SIZES, upper=True + ), + cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63), + cv.Optional(CONF_CONTRAST, default=0): camera_range_param, + cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, + cv.Optional(CONF_SATURATION, default=0): camera_range_param, + cv.Optional(CONF_VERTICAL_FLIP, default=True): cv.boolean, + cv.Optional(CONF_HORIZONTAL_MIRROR, default=True): cv.boolean, + cv.Optional(CONF_SPECIAL_EFFECT, default="NONE"): cv.enum( + ENUM_SPECIAL_EFFECT, upper=True + ), + # exposure + cv.Optional(CONF_AGC_MODE, default="AUTO"): cv.enum( + ENUM_GAIN_CONTROL_MODE, upper=True + ), + cv.Optional(CONF_AEC2, default=False): cv.boolean, + cv.Optional(CONF_AE_LEVEL, default=0): camera_range_param, + cv.Optional(CONF_AEC_VALUE, default=300): cv.int_range(min=0, max=1200), + # gains + cv.Optional(CONF_AEC_MODE, default="AUTO"): cv.enum( + ENUM_GAIN_CONTROL_MODE, upper=True + ), + cv.Optional(CONF_AGC_VALUE, default=0): cv.int_range(min=0, max=30), + cv.Optional(CONF_AGC_GAIN_CEILING, default="2X"): cv.enum( + ENUM_GAIN_CEILING, upper=True + ), + # white balance + cv.Optional(CONF_WB_MODE, default="AUTO"): cv.enum( + ENUM_WB_MODE, upper=True + ), + # test pattern + cv.Optional(CONF_TEST_PATTERN, default=False): cv.boolean, + # framerates + cv.Optional(CONF_MAX_FRAMERATE, default="10 fps"): cv.All( + cv.framerate, cv.Range(min=0, min_included=False, max=60) + ), + cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All( + cv.framerate, cv.Range(min=0, max=1) + ), + cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), + cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32CameraStreamStartTrigger + ), + } + ), + cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32CameraStreamStopTrigger + ), + } + ), + cv.Optional(CONF_ON_IMAGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32CameraImageTrigger + ), + } + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID), +) SETTERS = { # pin assignment @@ -280,8 +293,12 @@ async def to_code(config): extclk = config[CONF_EXTERNAL_CLOCK] cg.add(var.set_external_clock(extclk[CONF_PIN], extclk[CONF_FREQUENCY])) - i2c_pins = config[CONF_I2C_PINS] - cg.add(var.set_i2c_pins(i2c_pins[CONF_SDA], i2c_pins[CONF_SCL])) + if i2c_id := config.get(CONF_I2C_ID): + i2c_hub = await cg.get_variable(i2c_id) + cg.add(var.set_i2c_id(i2c_hub)) + else: + i2c_pins = config[CONF_I2C_PINS] + cg.add(var.set_i2c_pins(i2c_pins[CONF_SDA], i2c_pins[CONF_SCL])) cg.add(var.set_max_update_interval(1000 / config[CONF_MAX_FRAMERATE])) if config[CONF_IDLE_FRAMERATE] == 0: cg.add(var.set_idle_update_interval(0)) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index da0f277358..243d3d3e47 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -1,9 +1,9 @@ #ifdef USE_ESP32 #include "esp32_camera.h" -#include "esphome/core/log.h" -#include "esphome/core/hal.h" #include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" #include @@ -16,6 +16,12 @@ static const char *const TAG = "esp32_camera"; void ESP32Camera::setup() { global_esp32_camera = this; +#ifdef USE_I2C + if (this->i2c_bus_ != nullptr) { + this->config_.sccb_i2c_port = this->i2c_bus_->get_port(); + } +#endif + /* initialize time to now */ this->last_update_ = millis(); @@ -246,6 +252,13 @@ void ESP32Camera::set_i2c_pins(uint8_t sda, uint8_t scl) { this->config_.pin_sccb_sda = sda; this->config_.pin_sccb_scl = scl; } +#ifdef USE_I2C +void ESP32Camera::set_i2c_id(i2c::InternalI2CBus *i2c_bus) { + this->i2c_bus_ = i2c_bus; + this->config_.pin_sccb_sda = -1; + this->config_.pin_sccb_scl = -1; +} +#endif // USE_I2C void ESP32Camera::set_reset_pin(uint8_t pin) { this->config_.pin_reset = pin; } void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; } diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index d5fe48c2a7..75139ba400 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -2,13 +2,17 @@ #ifdef USE_ESP32 +#include +#include +#include #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" -#include -#include -#include + +#ifdef USE_I2C +#include "esphome/components/i2c/i2c_bus.h" +#endif // USE_I2C namespace esphome { namespace esp32_camera { @@ -118,6 +122,9 @@ class ESP32Camera : public EntityBase, public Component { void set_pixel_clock_pin(uint8_t pin); void set_external_clock(uint8_t pin, uint32_t frequency); void set_i2c_pins(uint8_t sda, uint8_t scl); +#ifdef USE_I2C + void set_i2c_id(i2c::InternalI2CBus *i2c_bus); +#endif // USE_I2C void set_reset_pin(uint8_t pin); void set_power_down_pin(uint8_t pin); /* -- image */ @@ -210,6 +217,9 @@ class ESP32Camera : public EntityBase, public Component { uint32_t last_idle_request_{0}; uint32_t last_update_{0}; +#ifdef USE_I2C + i2c::InternalI2CBus *i2c_bus_{nullptr}; +#endif // USE_I2C }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) From eb97781f684166d68872fe68db6a96db4aa000b8 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Fri, 20 Jun 2025 08:38:40 +0200 Subject: [PATCH 077/293] [nextion] Add command queuing to prevent command loss when spacing is active (#9139) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/nextion/nextion.cpp | 56 +++++++++++++++++++ esphome/components/nextion/nextion.h | 31 ++++++++++ .../nextion/nextion_component_base.h | 3 + 3 files changed, 90 insertions(+) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index e6fee10173..1e14831515 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -325,8 +325,31 @@ void Nextion::loop() { this->nextion_reports_is_setup_ = true; } } + +#ifdef USE_NEXTION_COMMAND_SPACING + // Try to send any pending commands if spacing allows + this->process_pending_in_queue_(); +#endif // USE_NEXTION_COMMAND_SPACING } +#ifdef USE_NEXTION_COMMAND_SPACING +void Nextion::process_pending_in_queue_() { + if (this->nextion_queue_.empty() || !this->command_pacer_.can_send()) { + return; + } + + // Check if first item in queue has a pending command + auto *front_item = this->nextion_queue_.front(); + if (front_item && !front_item->pending_command.empty()) { + if (this->send_command_(front_item->pending_command)) { + // Command sent successfully, clear the pending command + front_item->pending_command.clear(); + ESP_LOGVV(TAG, "Pending command sent: %s", front_item->component->get_variable_name().c_str()); + } + } +} +#endif // USE_NEXTION_COMMAND_SPACING + bool Nextion::remove_from_q_(bool report_empty) { if (this->nextion_queue_.empty()) { if (report_empty) { @@ -1034,9 +1057,42 @@ void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_n if (this->send_command_(command)) { this->add_no_result_to_queue_(variable_name); +#ifdef USE_NEXTION_COMMAND_SPACING + } else { + // Command blocked by spacing, add to queue WITH the command for retry + this->add_no_result_to_queue_with_pending_command_(variable_name, command); +#endif // USE_NEXTION_COMMAND_SPACING } } +#ifdef USE_NEXTION_COMMAND_SPACING +void Nextion::add_no_result_to_queue_with_pending_command_(const std::string &variable_name, + const std::string &command) { +#ifdef USE_NEXTION_MAX_QUEUE_SIZE + if (this->max_queue_size_ > 0 && this->nextion_queue_.size() >= this->max_queue_size_) { + ESP_LOGW(TAG, "Queue full (%zu), drop: %s", this->nextion_queue_.size(), variable_name.c_str()); + return; + } +#endif + + RAMAllocator allocator; + nextion::NextionQueue *nextion_queue = allocator.allocate(1); + if (nextion_queue == nullptr) { + ESP_LOGW(TAG, "Queue alloc failed"); + return; + } + new (nextion_queue) nextion::NextionQueue(); + + nextion_queue->component = new nextion::NextionComponentBase; + nextion_queue->component->set_variable_name(variable_name); + nextion_queue->queue_time = millis(); + nextion_queue->pending_command = command; // Store command for retry + + this->nextion_queue_.push_back(nextion_queue); + ESP_LOGVV(TAG, "Queue with pending command: %s", variable_name.c_str()); +} +#endif // USE_NEXTION_COMMAND_SPACING + bool Nextion::add_no_result_to_queue_with_ignore_sleep_printf_(const std::string &variable_name, const char *format, ...) { if ((!this->is_setup() && !this->ignore_is_setup_)) diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 036fbe6c6d..0cd559d251 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1309,9 +1309,23 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe #ifdef USE_NEXTION_MAX_QUEUE_SIZE size_t max_queue_size_{0}; #endif // USE_NEXTION_MAX_QUEUE_SIZE + #ifdef USE_NEXTION_COMMAND_SPACING NextionCommandPacer command_pacer_{0}; + + /** + * @brief Process any commands in the queue that are pending due to command spacing + * + * This method checks if the first item in the nextion_queue_ has a pending command + * that was previously blocked by command spacing. If spacing now allows and a + * pending command exists, it attempts to send the command. Once successfully sent, + * the pending command is cleared and the queue item continues normal processing. + * + * Called from loop() to retry sending commands that were delayed by spacing. + */ + void process_pending_in_queue_(); #endif // USE_NEXTION_COMMAND_SPACING + std::deque nextion_queue_; std::deque waveform_queue_; uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag); @@ -1348,6 +1362,23 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe __attribute__((format(printf, 3, 4))); void add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command); +#ifdef USE_NEXTION_COMMAND_SPACING + /** + * @brief Add a command to the Nextion queue with a pending command for retry + * + * This method creates a queue entry for a command that was blocked by command spacing. + * The command string is stored in the queue item's pending_command field so it can + * be retried later when spacing allows. This ensures commands are not lost when + * sent too quickly. + * + * If the max_queue_size limit is configured and reached, the command will be dropped. + * + * @param variable_name Name of the variable or component associated with the command + * @param command The actual command string to be sent when spacing allows + */ + void add_no_result_to_queue_with_pending_command_(const std::string &variable_name, const std::string &command); +#endif // USE_NEXTION_COMMAND_SPACING + bool add_no_result_to_queue_with_printf_(const std::string &variable_name, const char *format, ...) __attribute__((format(printf, 3, 4))); diff --git a/esphome/components/nextion/nextion_component_base.h b/esphome/components/nextion/nextion_component_base.h index 42e1b00998..fe0692b875 100644 --- a/esphome/components/nextion/nextion_component_base.h +++ b/esphome/components/nextion/nextion_component_base.h @@ -25,6 +25,9 @@ class NextionQueue { virtual ~NextionQueue() = default; NextionComponentBase *component; uint32_t queue_time = 0; + + // Store command for retry if spacing blocked it + std::string pending_command; // Empty if command was sent successfully }; class NextionComponentBase { From 7dbad424705ae3c7dafa477a2f815173c4359151 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Fri, 20 Jun 2025 09:46:12 +0200 Subject: [PATCH 078/293] [nextion] Cached timing optimization (#9150) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Keith Burzinski --- esphome/components/nextion/nextion.cpp | 26 +++++++++---------- .../nextion/nextion_upload_arduino.cpp | 4 +-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 1e14831515..4f08fcb393 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -71,13 +71,13 @@ bool Nextion::check_connect_() { } this->send_command_("connect"); - this->comok_sent_ = millis(); + this->comok_sent_ = App.get_loop_component_start_time(); this->ignore_is_setup_ = false; return false; } - if (millis() - this->comok_sent_ <= 500) // Wait 500 ms + if (App.get_loop_component_start_time() - this->comok_sent_ <= 500) // Wait 500 ms return false; std::string response; @@ -318,9 +318,9 @@ void Nextion::loop() { if (!this->nextion_reports_is_setup_) { if (this->started_ms_ == 0) - this->started_ms_ = millis(); + this->started_ms_ = App.get_loop_component_start_time(); - if (this->started_ms_ + this->startup_override_ms_ < millis()) { + if (this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) { ESP_LOGD(TAG, "Manual ready set"); this->nextion_reports_is_setup_ = true; } @@ -432,7 +432,7 @@ void Nextion::process_nextion_commands_() { case 0x01: // instruction sent by user was successful ESP_LOGVV(TAG, "Cmd OK"); - ESP_LOGN(TAG, "this->nextion_queue_.empty() %s", this->nextion_queue_.empty() ? "True" : "False"); + ESP_LOGN(TAG, "this->nextion_queue_.empty() %s", YESNO(this->nextion_queue_.empty())); this->remove_from_q_(); if (!this->is_setup_) { @@ -444,7 +444,7 @@ void Nextion::process_nextion_commands_() { } #ifdef USE_NEXTION_COMMAND_SPACING this->command_pacer_.mark_sent(); // Here is where we should mark the command as sent - ESP_LOGN(TAG, "Command spacing: marked command sent at %u ms", millis()); + ESP_LOGN(TAG, "Command spacing: marked command sent"); #endif break; case 0x02: // invalid Component ID or name was used @@ -828,7 +828,7 @@ void Nextion::process_nextion_commands_() { this->command_data_.erase(0, to_process_length + COMMAND_DELIMITER.length() + 1); } - uint32_t ms = millis(); + uint32_t ms = App.get_loop_component_start_time(); if (!this->nextion_queue_.empty() && this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) { for (size_t i = 0; i < this->nextion_queue_.size(); i++) { @@ -967,9 +967,9 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool bool exit_flag = false; bool ff_flag = false; - start = millis(); + start = App.get_loop_component_start_time(); - while ((timeout == 0 && this->available()) || millis() - start <= timeout) { + while ((timeout == 0 && this->available()) || App.get_loop_component_start_time() - start <= timeout) { if (!this->available()) { App.feed_wdt(); delay(1); @@ -1038,7 +1038,7 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) { nextion_queue->component = new nextion::NextionComponentBase; nextion_queue->component->set_variable_name(variable_name); - nextion_queue->queue_time = millis(); + nextion_queue->queue_time = App.get_loop_component_start_time(); this->nextion_queue_.push_back(nextion_queue); @@ -1085,7 +1085,7 @@ void Nextion::add_no_result_to_queue_with_pending_command_(const std::string &va nextion_queue->component = new nextion::NextionComponentBase; nextion_queue->component->set_variable_name(variable_name); - nextion_queue->queue_time = millis(); + nextion_queue->queue_time = App.get_loop_component_start_time(); nextion_queue->pending_command = command; // Store command for retry this->nextion_queue_.push_back(nextion_queue); @@ -1224,7 +1224,7 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) { new (nextion_queue) nextion::NextionQueue(); nextion_queue->component = component; - nextion_queue->queue_time = millis(); + nextion_queue->queue_time = App.get_loop_component_start_time(); ESP_LOGN(TAG, "Queue %s: %s", component->get_queue_type_string().c_str(), component->get_variable_name().c_str()); @@ -1256,7 +1256,7 @@ void Nextion::add_addt_command_to_queue(NextionComponentBase *component) { new (nextion_queue) nextion::NextionQueue(); nextion_queue->component = component; - nextion_queue->queue_time = millis(); + nextion_queue->queue_time = App.get_loop_component_start_time(); this->waveform_queue_.push_back(nextion_queue); if (this->waveform_queue_.size() == 1) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index aa7350bb57..c2d0f2a22d 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -67,8 +67,8 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { ESP_LOGV(TAG, "Fetch %" PRIu16 " bytes", buffer_size); uint16_t read_len = 0; int partial_read_len = 0; - const uint32_t start_time = millis(); - while (read_len < buffer_size && millis() - start_time < 5000) { + const uint32_t start_time = App.get_loop_component_start_time(); + while (read_len < buffer_size && App.get_loop_component_start_time() - start_time < 5000) { if (http_client.getStreamPtr()->available() > 0) { partial_read_len = http_client.getStreamPtr()->readBytes(reinterpret_cast(buffer) + read_len, buffer_size - read_len); From 46d962dcf1ddff27ce925b9a483e511a95af18d5 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Fri, 20 Jun 2025 05:02:36 -0500 Subject: [PATCH 079/293] [wifi, wifi_info] Tidy up/shorten more log messages (#9151) --- esphome/components/wifi/wifi_component.cpp | 34 +++---- .../wifi/wifi_component_esp32_arduino.cpp | 82 ++++++++--------- .../wifi/wifi_component_esp8266.cpp | 91 +++++++++---------- .../wifi/wifi_component_esp_idf.cpp | 67 +++++++------- .../wifi/wifi_component_libretiny.cpp | 49 +++++----- .../components/wifi/wifi_component_pico_w.cpp | 6 +- .../wifi_info/wifi_info_text_sensor.cpp | 12 +-- 7 files changed, 168 insertions(+), 173 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index f2f9d712fc..51ae1c9f8e 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -73,7 +73,7 @@ void WiFiComponent::start() { SavedWifiSettings save{}; if (this->pref_.load(&save)) { - ESP_LOGD(TAG, "Loaded saved settings: %s", save.ssid); + ESP_LOGD(TAG, "Loaded settings: %s", save.ssid); WiFiAP sta{}; sta.set_ssid(save.ssid); @@ -84,11 +84,11 @@ void WiFiComponent::start() { if (this->has_sta()) { this->wifi_sta_pre_setup_(); if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) { - ESP_LOGV(TAG, "Setting Output Power Option failed!"); + ESP_LOGV(TAG, "Setting Output Power Option failed"); } if (!this->wifi_apply_power_save_()) { - ESP_LOGV(TAG, "Setting Power Save Option failed!"); + ESP_LOGV(TAG, "Setting Power Save Option failed"); } if (this->fast_connect_) { @@ -102,7 +102,7 @@ void WiFiComponent::start() { } else if (this->has_ap()) { this->setup_ap_config_(); if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) { - ESP_LOGV(TAG, "Setting Output Power Option failed!"); + ESP_LOGV(TAG, "Setting Output Power Option failed"); } #ifdef USE_CAPTIVE_PORTAL if (captive_portal::global_captive_portal != nullptr) { @@ -181,7 +181,7 @@ void WiFiComponent::loop() { #ifdef USE_WIFI_AP if (this->has_ap() && !this->ap_setup_) { if (this->ap_timeout_ != 0 && (now - this->last_connected_ > this->ap_timeout_)) { - ESP_LOGI(TAG, "Starting fallback AP!"); + ESP_LOGI(TAG, "Starting fallback AP"); this->setup_ap_config_(); #ifdef USE_CAPTIVE_PORTAL if (captive_portal::global_captive_portal != nullptr) @@ -359,7 +359,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { if (ap.get_channel().has_value()) { ESP_LOGV(TAG, " Channel: %u", *ap.get_channel()); } else { - ESP_LOGV(TAG, " Channel: Not Set"); + ESP_LOGV(TAG, " Channel not set"); } if (ap.get_manual_ip().has_value()) { ManualIP m = *ap.get_manual_ip(); @@ -372,7 +372,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { #endif if (!this->wifi_sta_connect_(ap)) { - ESP_LOGE(TAG, "wifi_sta_connect_ failed!"); + ESP_LOGE(TAG, "wifi_sta_connect_ failed"); this->retry_connect(); return; } @@ -500,20 +500,20 @@ void WiFiComponent::start_scanning() { void WiFiComponent::check_scanning_finished() { if (!this->scan_done_) { if (millis() - this->action_started_ > 30000) { - ESP_LOGE(TAG, "Scan timeout!"); + ESP_LOGE(TAG, "Scan timeout"); this->retry_connect(); } return; } this->scan_done_ = false; - ESP_LOGD(TAG, "Found networks:"); if (this->scan_result_.empty()) { - ESP_LOGD(TAG, " No network found!"); + ESP_LOGW(TAG, "No networks found"); this->retry_connect(); return; } + ESP_LOGD(TAG, "Found networks:"); for (auto &res : this->scan_result_) { for (auto &ap : this->sta_) { if (res.matches(ap)) { @@ -561,7 +561,7 @@ void WiFiComponent::check_scanning_finished() { } if (!this->scan_result_[0].get_matches()) { - ESP_LOGW(TAG, "No matching network found!"); + ESP_LOGW(TAG, "No matching network found"); this->retry_connect(); return; } @@ -619,7 +619,7 @@ void WiFiComponent::check_connecting_finished() { if (status == WiFiSTAConnectStatus::CONNECTED) { if (wifi_ssid().empty()) { - ESP_LOGW(TAG, "Incomplete connection."); + ESP_LOGW(TAG, "Connection incomplete"); this->retry_connect(); return; } @@ -663,7 +663,7 @@ void WiFiComponent::check_connecting_finished() { } if (this->error_from_callback_) { - ESP_LOGW(TAG, "Error while connecting to network."); + ESP_LOGW(TAG, "Connecting to network failed"); this->retry_connect(); return; } @@ -679,7 +679,7 @@ void WiFiComponent::check_connecting_finished() { } if (status == WiFiSTAConnectStatus::ERROR_CONNECT_FAILED) { - ESP_LOGW(TAG, "Connection failed. Check credentials"); + ESP_LOGW(TAG, "Connecting to network failed"); this->retry_connect(); return; } @@ -700,7 +700,7 @@ void WiFiComponent::retry_connect() { (this->num_retried_ > 3 || this->error_from_callback_)) { if (this->num_retried_ > 5) { // If retry failed for more than 5 times, let's restart STA - ESP_LOGW(TAG, "Restarting WiFi adapter"); + ESP_LOGW(TAG, "Restarting adapter"); this->wifi_mode_(false, {}); delay(100); // NOLINT this->num_retried_ = 0; @@ -770,7 +770,7 @@ void WiFiComponent::load_fast_connect_settings_() { this->selected_ap_.set_bssid(bssid); this->selected_ap_.set_channel(fast_connect_save.channel); - ESP_LOGD(TAG, "Loaded saved fast_connect wifi settings"); + ESP_LOGD(TAG, "Loaded fast_connect settings"); } } @@ -786,7 +786,7 @@ void WiFiComponent::save_fast_connect_settings_() { this->fast_connect_pref_.save(&fast_connect_save); - ESP_LOGD(TAG, "Saved fast_connect wifi settings"); + ESP_LOGD(TAG, "Saved fast_connect settings"); } } diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index d9e45242a8..3fc2c009db 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -78,14 +78,14 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { return true; if (set_sta && !current_sta) { - ESP_LOGV(TAG, "Enabling STA."); + ESP_LOGV(TAG, "Enabling STA"); } else if (!set_sta && current_sta) { - ESP_LOGV(TAG, "Disabling STA."); + ESP_LOGV(TAG, "Disabling STA"); } if (set_ap && !current_ap) { - ESP_LOGV(TAG, "Enabling AP."); + ESP_LOGV(TAG, "Enabling AP"); } else if (!set_ap && current_ap) { - ESP_LOGV(TAG, "Disabling AP."); + ESP_LOGV(TAG, "Disabling AP"); } bool ret = WiFiClass::mode(set_mode); @@ -147,11 +147,11 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) { - ESP_LOGE(TAG, "SSID is too long"); + ESP_LOGE(TAG, "SSID too long"); return false; } if (ap.get_password().size() > sizeof(conf.sta.password)) { - ESP_LOGE(TAG, "password is too long"); + ESP_LOGE(TAG, "Password too long"); return false; } memcpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); @@ -230,7 +230,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { EAPAuth eap = ap.get_eap().value(); err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_eap_client_set_identity failed! %d", err); + ESP_LOGV(TAG, "esp_eap_client_set_identity failed: %d", err); } int ca_cert_len = strlen(eap.ca_cert); int client_cert_len = strlen(eap.client_cert); @@ -238,7 +238,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (ca_cert_len) { err = esp_eap_client_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_eap_client_set_ca_cert failed! %d", err); + ESP_LOGV(TAG, "esp_eap_client_set_ca_cert failed: %d", err); } } // workout what type of EAP this is @@ -249,22 +249,22 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { (uint8_t *) eap.client_key, client_key_len + 1, (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_eap_client_set_certificate_and_key failed! %d", err); + ESP_LOGV(TAG, "esp_eap_client_set_certificate_and_key failed: %d", err); } } else { // in the absence of certs, assume this is username/password based err = esp_eap_client_set_username((uint8_t *) eap.username.c_str(), eap.username.length()); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_eap_client_set_username failed! %d", err); + ESP_LOGV(TAG, "esp_eap_client_set_username failed: %d", err); } err = esp_eap_client_set_password((uint8_t *) eap.password.c_str(), eap.password.length()); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_eap_client_set_password failed! %d", err); + ESP_LOGV(TAG, "esp_eap_client_set_password failed: %d", err); } } err = esp_wifi_sta_enterprise_enable(); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_enterprise_enable failed! %d", err); + ESP_LOGV(TAG, "esp_wifi_sta_enterprise_enable failed: %d", err); } } #endif // USE_WIFI_WPA2_EAP @@ -319,7 +319,7 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { if (dhcp_status != ESP_NETIF_DHCP_STARTED) { err = esp_netif_dhcpc_start(s_sta_netif); if (err != ESP_OK) { - ESP_LOGV(TAG, "Starting DHCP client failed! %d", err); + ESP_LOGV(TAG, "Starting DHCP client failed: %d", err); } return err == ESP_OK; } @@ -332,12 +332,12 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { info.netmask = manual_ip->subnet; err = esp_netif_dhcpc_stop(s_sta_netif); if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { - ESP_LOGV(TAG, "Stopping DHCP client failed! %s", esp_err_to_name(err)); + ESP_LOGV(TAG, "Stopping DHCP client failed: %s", esp_err_to_name(err)); } err = esp_netif_set_ip_info(s_sta_netif, &info); if (err != ESP_OK) { - ESP_LOGV(TAG, "Setting manual IP info failed! %s", esp_err_to_name(err)); + ESP_LOGV(TAG, "Setting manual IP info failed: %s", esp_err_to_name(err)); } esp_netif_dns_info_t dns; @@ -520,18 +520,18 @@ using esphome_wifi_event_info_t = arduino_event_info_t; void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_wifi_event_info_t info) { switch (event) { case ESPHOME_EVENT_ID_WIFI_READY: { - ESP_LOGV(TAG, "Event: WiFi ready"); + ESP_LOGV(TAG, "Ready"); break; } case ESPHOME_EVENT_ID_WIFI_SCAN_DONE: { auto it = info.wifi_scan_done; - ESP_LOGV(TAG, "Event: WiFi Scan Done status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id); + ESP_LOGV(TAG, "Scan done: status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id); this->wifi_scan_done_callback_(); break; } case ESPHOME_EVENT_ID_WIFI_STA_START: { - ESP_LOGV(TAG, "Event: WiFi STA start"); + ESP_LOGV(TAG, "STA start"); // apply hostname s_sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); esp_err_t err = esp_netif_set_hostname(s_sta_netif, App.get_name().c_str()); @@ -541,7 +541,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ break; } case ESPHOME_EVENT_ID_WIFI_STA_STOP: { - ESP_LOGV(TAG, "Event: WiFi STA stop"); + ESP_LOGV(TAG, "STA stop"); break; } case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: { @@ -549,7 +549,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ char buf[33]; memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; - ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, + ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); #if USE_NETWORK_IPV6 this->set_timeout(100, [] { WiFi.enableIPv6(); }); @@ -563,9 +563,9 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; if (it.reason == WIFI_REASON_NO_AP_FOUND) { - ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); + ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); } else { - ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); } @@ -585,8 +585,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: { auto it = info.wifi_sta_authmode_change; - ESP_LOGV(TAG, "Event: Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), - get_auth_mode_str(it.new_mode)); + ESP_LOGV(TAG, "Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode)); // Mitigate CVE-2020-12638 // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) { @@ -603,8 +602,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP: { auto it = info.got_ip.ip_info; - ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(it.ip).c_str(), - format_ip4_addr(it.gw).c_str()); + ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip).c_str(), format_ip4_addr(it.gw).c_str()); this->got_ipv4_address_ = true; #if USE_NETWORK_IPV6 s_sta_connecting = this->num_ipv6_addresses_ < USE_NETWORK_MIN_IPV6_ADDR_COUNT; @@ -616,44 +614,44 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ #if USE_NETWORK_IPV6 case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { auto it = info.got_ip6.ip6_info; - ESP_LOGV(TAG, "Got IPv6 address=" IPV6STR, IPV62STR(it.ip)); + ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip)); this->num_ipv6_addresses_++; s_sta_connecting = !(this->got_ipv4_address_ & (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT)); break; } #endif /* USE_NETWORK_IPV6 */ case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: { - ESP_LOGV(TAG, "Event: Lost IP"); + ESP_LOGV(TAG, "Lost IP"); this->got_ipv4_address_ = false; break; } case ESPHOME_EVENT_ID_WIFI_AP_START: { - ESP_LOGV(TAG, "Event: WiFi AP start"); + ESP_LOGV(TAG, "AP start"); break; } case ESPHOME_EVENT_ID_WIFI_AP_STOP: { - ESP_LOGV(TAG, "Event: WiFi AP stop"); + ESP_LOGV(TAG, "AP stop"); break; } case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: { auto it = info.wifi_sta_connected; auto &mac = it.bssid; - ESP_LOGV(TAG, "Event: AP client connected MAC=%s", format_mac_addr(mac).c_str()); + ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_addr(mac).c_str()); break; } case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: { auto it = info.wifi_sta_disconnected; auto &mac = it.bssid; - ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s", format_mac_addr(mac).c_str()); + ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_addr(mac).c_str()); break; } case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: { - ESP_LOGV(TAG, "Event: AP client assigned IP"); + ESP_LOGV(TAG, "AP client assigned IP"); break; } case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: { auto it = info.wifi_ap_probereqrecved; - ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); break; } default: @@ -685,7 +683,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { // need to use WiFi because of WiFiScanClass allocations :( int16_t err = WiFi.scanNetworks(true, true, passive, 200); if (err != WIFI_SCAN_RUNNING) { - ESP_LOGV(TAG, "WiFi.scanNetworks failed! %d", err); + ESP_LOGV(TAG, "WiFi.scanNetworks failed: %d", err); return false; } @@ -741,7 +739,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { err = esp_netif_set_ip_info(s_ap_netif, &info); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_netif_set_ip_info failed! %d", err); + ESP_LOGE(TAG, "esp_netif_set_ip_info failed: %d", err); return false; } @@ -757,14 +755,14 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease)); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_netif_dhcps_option failed! %d", err); + ESP_LOGE(TAG, "esp_netif_dhcps_option failed: %d", err); return false; } err = esp_netif_dhcps_start(s_ap_netif); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_netif_dhcps_start failed! %d", err); + ESP_LOGE(TAG, "esp_netif_dhcps_start failed: %d", err); return false; } @@ -779,7 +777,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) { - ESP_LOGE(TAG, "AP SSID is too long"); + ESP_LOGE(TAG, "AP SSID too long"); return false; } memcpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); @@ -794,7 +792,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { } else { conf.ap.authmode = WIFI_AUTH_WPA2_PSK; if (ap.get_password().size() > sizeof(conf.ap.password)) { - ESP_LOGE(TAG, "AP password is too long"); + ESP_LOGE(TAG, "AP password too long"); return false; } memcpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), ap.get_password().size()); @@ -805,14 +803,14 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_set_config failed! %d", err); + ESP_LOGV(TAG, "esp_wifi_set_config failed: %d", err); return false; } yield(); if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { - ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!"); + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 3e121098e7..594bc79e54 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -59,17 +59,17 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { return true; if (target_sta && !current_sta) { - ESP_LOGV(TAG, "Enabling STA."); + ESP_LOGV(TAG, "Enabling STA"); } else if (!target_sta && current_sta) { - ESP_LOGV(TAG, "Disabling STA."); + ESP_LOGV(TAG, "Disabling STA"); // Stop DHCP client when disabling STA // See https://github.com/esp8266/Arduino/pull/5703 wifi_station_dhcpc_stop(); } if (target_ap && !current_ap) { - ESP_LOGV(TAG, "Enabling AP."); + ESP_LOGV(TAG, "Enabling AP"); } else if (!target_ap && current_ap) { - ESP_LOGV(TAG, "Disabling AP."); + ESP_LOGV(TAG, "Disabling AP"); } ETS_UART_INTR_DISABLE(); @@ -82,7 +82,7 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { ETS_UART_INTR_ENABLE(); if (!ret) { - ESP_LOGW(TAG, "Setting WiFi mode failed!"); + ESP_LOGW(TAG, "Set mode failed"); } return ret; @@ -133,7 +133,7 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { if (dhcp_status != DHCP_STARTED) { bool ret = wifi_station_dhcpc_start(); if (!ret) { - ESP_LOGV(TAG, "Starting DHCP client failed!"); + ESP_LOGV(TAG, "Starting DHCP client failed"); } return ret; } @@ -157,13 +157,13 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { if (dhcp_status == DHCP_STARTED) { bool dhcp_stop_ret = wifi_station_dhcpc_stop(); if (!dhcp_stop_ret) { - ESP_LOGV(TAG, "Stopping DHCP client failed!"); + ESP_LOGV(TAG, "Stopping DHCP client failed"); ret = false; } } bool wifi_set_info_ret = wifi_set_ip_info(STATION_IF, &info); if (!wifi_set_info_ret) { - ESP_LOGV(TAG, "Setting manual IP info failed!"); + ESP_LOGV(TAG, "Set manual IP info failed"); ret = false; } @@ -202,7 +202,7 @@ bool WiFiComponent::wifi_apply_hostname_() { const std::string &hostname = App.get_name(); bool ret = wifi_station_set_hostname(const_cast(hostname.c_str())); if (!ret) { - ESP_LOGV(TAG, "Setting WiFi Hostname failed!"); + ESP_LOGV(TAG, "Set hostname failed"); } // inform dhcp server of hostname change using dhcp_renew() @@ -237,11 +237,11 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { struct station_config conf {}; memset(&conf, 0, sizeof(conf)); if (ap.get_ssid().size() > sizeof(conf.ssid)) { - ESP_LOGE(TAG, "SSID is too long"); + ESP_LOGE(TAG, "SSID too long"); return false; } if (ap.get_password().size() > sizeof(conf.password)) { - ESP_LOGE(TAG, "password is too long"); + ESP_LOGE(TAG, "Password too long"); return false; } memcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); @@ -269,7 +269,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { ETS_UART_INTR_ENABLE(); if (!ret) { - ESP_LOGV(TAG, "Setting WiFi Station config failed!"); + ESP_LOGV(TAG, "Set Station config failed"); return false; } @@ -284,7 +284,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { EAPAuth eap = ap.get_eap().value(); ret = wifi_station_set_enterprise_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); if (ret) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed! %d", ret); + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed: %d", ret); } int ca_cert_len = strlen(eap.ca_cert); int client_cert_len = strlen(eap.client_cert); @@ -292,7 +292,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (ca_cert_len) { ret = wifi_station_set_enterprise_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1); if (ret) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed! %d", ret); + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed: %d", ret); } } // workout what type of EAP this is @@ -303,22 +303,22 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { (uint8_t *) eap.client_key, client_key_len + 1, (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); if (ret) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed! %d", ret); + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed: %d", ret); } } else { // in the absence of certs, assume this is username/password based ret = wifi_station_set_enterprise_username((uint8_t *) eap.username.c_str(), eap.username.length()); if (ret) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed! %d", ret); + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed: %d", ret); } ret = wifi_station_set_enterprise_password((uint8_t *) eap.password.c_str(), eap.password.length()); if (ret) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed! %d", ret); + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed: %d", ret); } } ret = wifi_station_set_wpa2_enterprise_auth(true); if (ret) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", ret); + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed: %d", ret); } } #endif // USE_WIFI_WPA2_EAP @@ -337,7 +337,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { ret = wifi_station_connect(); ETS_UART_INTR_ENABLE(); if (!ret) { - ESP_LOGV(TAG, "wifi_station_connect failed!"); + ESP_LOGV(TAG, "wifi_station_connect failed"); return false; } @@ -359,7 +359,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (ap.get_channel().has_value()) { ret = wifi_set_channel(*ap.get_channel()); if (!ret) { - ESP_LOGV(TAG, "wifi_set_channel failed!"); + ESP_LOGV(TAG, "wifi_set_channel failed"); return false; } } @@ -496,8 +496,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { char buf[33]; memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; - ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_addr(it.bssid).c_str(), - it.channel); + ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_addr(it.bssid).c_str(), it.channel); s_sta_connected = true; break; } @@ -507,10 +506,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; if (it.reason == REASON_NO_AP_FOUND) { - ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); + ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); s_sta_connect_not_found = true; } else { - ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, format_mac_addr(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason))); s_sta_connect_error = true; } @@ -520,7 +519,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } case EVENT_STAMODE_AUTHMODE_CHANGE: { auto it = event->event_info.auth_change; - ESP_LOGV(TAG, "Event: Changed AuthMode old=%s new=%s", LOG_STR_ARG(get_auth_mode_str(it.old_mode)), + ESP_LOGV(TAG, "Changed Authmode old=%s new=%s", LOG_STR_ARG(get_auth_mode_str(it.old_mode)), LOG_STR_ARG(get_auth_mode_str(it.new_mode))); // Mitigate CVE-2020-12638 // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors @@ -535,40 +534,40 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } case EVENT_STAMODE_GOT_IP: { auto it = event->event_info.got_ip; - ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(), - format_ip_addr(it.gw).c_str(), format_ip_addr(it.mask).c_str()); + ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(), format_ip_addr(it.gw).c_str(), + format_ip_addr(it.mask).c_str()); s_sta_got_ip = true; break; } case EVENT_STAMODE_DHCP_TIMEOUT: { - ESP_LOGW(TAG, "Event: Getting IP address timeout"); + ESP_LOGW(TAG, "DHCP request timeout"); break; } case EVENT_SOFTAPMODE_STACONNECTED: { auto it = event->event_info.sta_connected; - ESP_LOGV(TAG, "Event: AP client connected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); + ESP_LOGV(TAG, "AP client connected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); break; } case EVENT_SOFTAPMODE_STADISCONNECTED: { auto it = event->event_info.sta_disconnected; - ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); + ESP_LOGV(TAG, "AP client disconnected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); break; } case EVENT_SOFTAPMODE_PROBEREQRECVED: { auto it = event->event_info.ap_probereqrecved; - ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); break; } #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) case EVENT_OPMODE_CHANGED: { auto it = event->event_info.opmode_changed; - ESP_LOGV(TAG, "Event: Changed Mode old=%s new=%s", LOG_STR_ARG(get_op_mode_str(it.old_opmode)), + ESP_LOGV(TAG, "Changed Mode old=%s new=%s", LOG_STR_ARG(get_op_mode_str(it.old_opmode)), LOG_STR_ARG(get_op_mode_str(it.new_opmode))); break; } case EVENT_SOFTAPMODE_DISTRIBUTE_STA_IP: { auto it = event->event_info.distribute_sta_ip; - ESP_LOGV(TAG, "Event: AP Distribute Station IP MAC=%s IP=%s aid=%u", format_mac_addr(it.mac).c_str(), + ESP_LOGV(TAG, "AP Distribute Station IP MAC=%s IP=%s aid=%u", format_mac_addr(it.mac).c_str(), format_ip_addr(it.ip).c_str(), it.aid); break; } @@ -600,7 +599,7 @@ bool WiFiComponent::wifi_sta_pre_setup_() { ETS_UART_INTR_ENABLE(); if (!ret1 || !ret2) { - ESP_LOGV(TAG, "Disabling Auto-Connect failed!"); + ESP_LOGV(TAG, "Disabling Auto-Connect failed"); } delay(10); @@ -666,7 +665,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { first_scan = false; bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback); if (!ret) { - ESP_LOGV(TAG, "wifi_station_scan failed!"); + ESP_LOGV(TAG, "wifi_station_scan failed"); return false; } @@ -692,7 +691,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { this->scan_result_.clear(); if (status != OK) { - ESP_LOGV(TAG, "Scan failed! %d", status); + ESP_LOGV(TAG, "Scan failed: %d", status); this->retry_connect(); return; } @@ -725,12 +724,12 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { if (wifi_softap_dhcps_status() == DHCP_STARTED) { if (!wifi_softap_dhcps_stop()) { - ESP_LOGW(TAG, "Stopping DHCP server failed!"); + ESP_LOGW(TAG, "Stopping DHCP server failed"); } } if (!wifi_set_ip_info(SOFTAP_IF, &info)) { - ESP_LOGE(TAG, "Setting SoftAP info failed!"); + ESP_LOGE(TAG, "Set SoftAP info failed"); return false; } @@ -748,13 +747,13 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { lease.end_ip = start_address; ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str().c_str()); if (!wifi_softap_set_dhcps_lease(&lease)) { - ESP_LOGE(TAG, "Setting SoftAP DHCP lease failed!"); + ESP_LOGE(TAG, "Set SoftAP DHCP lease failed"); return false; } // lease time 1440 minutes (=24 hours) if (!wifi_softap_set_dhcps_lease_time(1440)) { - ESP_LOGE(TAG, "Setting SoftAP DHCP lease time failed!"); + ESP_LOGE(TAG, "Set SoftAP DHCP lease time failed"); return false; } @@ -764,13 +763,13 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { uint8_t mode = 1; // bit0, 1 enables router information from ESP8266 SoftAP DHCP server. if (!wifi_softap_set_dhcps_offer_option(OFFER_ROUTER, &mode)) { - ESP_LOGE(TAG, "wifi_softap_set_dhcps_offer_option failed!"); + ESP_LOGE(TAG, "wifi_softap_set_dhcps_offer_option failed"); return false; } #endif if (!wifi_softap_dhcps_start()) { - ESP_LOGE(TAG, "Starting SoftAP DHCPS failed!"); + ESP_LOGE(TAG, "Starting SoftAP DHCPS failed"); return false; } @@ -784,7 +783,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { struct softap_config conf {}; if (ap.get_ssid().size() > sizeof(conf.ssid)) { - ESP_LOGE(TAG, "AP SSID is too long"); + ESP_LOGE(TAG, "AP SSID too long"); return false; } memcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); @@ -800,7 +799,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { } else { conf.authmode = AUTH_WPA2_PSK; if (ap.get_password().size() > sizeof(conf.password)) { - ESP_LOGE(TAG, "AP password is too long"); + ESP_LOGE(TAG, "AP password too long"); return false; } memcpy(reinterpret_cast(conf.password), ap.get_password().c_str(), ap.get_password().size()); @@ -811,12 +810,12 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { ETS_UART_INTR_ENABLE(); if (!ret) { - ESP_LOGV(TAG, "wifi_softap_set_config_current failed!"); + ESP_LOGV(TAG, "wifi_softap_set_config_current failed"); return false; } if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { - ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!"); + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 1af271345f..e767e7ffc1 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -219,14 +219,14 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { return true; if (set_sta && !current_sta) { - ESP_LOGV(TAG, "Enabling STA."); + ESP_LOGV(TAG, "Enabling STA"); } else if (!set_sta && current_sta) { - ESP_LOGV(TAG, "Disabling STA."); + ESP_LOGV(TAG, "Disabling STA"); } if (set_ap && !current_ap) { - ESP_LOGV(TAG, "Enabling AP."); + ESP_LOGV(TAG, "Enabling AP"); } else if (!set_ap && current_ap) { - ESP_LOGV(TAG, "Disabling AP."); + ESP_LOGV(TAG, "Disabling AP"); } if (set_mode == WIFI_MODE_NULL && s_wifi_started) { @@ -290,11 +290,11 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) { - ESP_LOGE(TAG, "SSID is too long"); + ESP_LOGE(TAG, "SSID too long"); return false; } if (ap.get_password().size() > sizeof(conf.sta.password)) { - ESP_LOGE(TAG, "password is too long"); + ESP_LOGE(TAG, "Password too long"); return false; } memcpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); @@ -490,7 +490,7 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { if (dhcp_status != ESP_NETIF_DHCP_STARTED) { err = esp_netif_dhcpc_start(s_sta_netif); if (err != ESP_OK) { - ESP_LOGV(TAG, "Starting DHCP client failed! %d", err); + ESP_LOGV(TAG, "Starting DHCP client failed: %d", err); } return err == ESP_OK; } @@ -503,12 +503,12 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { info.netmask = manual_ip->subnet; err = esp_netif_dhcpc_stop(s_sta_netif); if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { - ESP_LOGV(TAG, "Stopping DHCP client failed! %s", esp_err_to_name(err)); + ESP_LOGV(TAG, "Stopping DHCP client failed: %s", esp_err_to_name(err)); } err = esp_netif_set_ip_info(s_sta_netif, &info); if (err != ESP_OK) { - ESP_LOGV(TAG, "Setting manual IP info failed! %s", esp_err_to_name(err)); + ESP_LOGV(TAG, "Setting manual IP info failed: %s", esp_err_to_name(err)); } esp_netif_dns_info_t dns; @@ -665,7 +665,7 @@ void WiFiComponent::wifi_loop_() { void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { esp_err_t err; if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_START) { - ESP_LOGV(TAG, "Event: WiFi STA start"); + ESP_LOGV(TAG, "STA start"); // apply hostname err = esp_netif_set_hostname(s_sta_netif, App.get_name().c_str()); if (err != ERR_OK) { @@ -677,13 +677,12 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { wifi_apply_power_save_(); } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) { - ESP_LOGV(TAG, "Event: WiFi STA stop"); + ESP_LOGV(TAG, "STA stop"); s_sta_started = false; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) { const auto &it = data->data.sta_authmode_change; - ESP_LOGV(TAG, "Event: Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), - get_auth_mode_str(it.new_mode)); + ESP_LOGV(TAG, "Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode)); } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_CONNECTED) { const auto &it = data->data.sta_connected; @@ -691,7 +690,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { assert(it.ssid_len <= 32); memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; - ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, + ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); s_sta_connected = true; @@ -702,13 +701,13 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; if (it.reason == WIFI_REASON_NO_AP_FOUND) { - ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); + ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); s_sta_connect_not_found = true; } else if (it.reason == WIFI_REASON_ROAMING) { - ESP_LOGI(TAG, "Event: Disconnected ssid='%s' reason='Station Roaming'", buf); + ESP_LOGI(TAG, "Disconnected ssid='%s' reason='Station Roaming'", buf); return; } else { - ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); s_sta_connect_error = true; } @@ -721,24 +720,24 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { #if USE_NETWORK_IPV6 esp_netif_create_ip6_linklocal(s_sta_netif); #endif /* USE_NETWORK_IPV6 */ - ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(it.ip_info.ip).c_str(), + ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip_info.ip).c_str(), format_ip4_addr(it.ip_info.gw).c_str()); this->got_ipv4_address_ = true; #if USE_NETWORK_IPV6 } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) { const auto &it = data->data.ip_got_ip6; - ESP_LOGV(TAG, "Event: Got IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str()); + ESP_LOGV(TAG, "IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str()); this->num_ipv6_addresses_++; #endif /* USE_NETWORK_IPV6 */ } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_LOST_IP) { - ESP_LOGV(TAG, "Event: Lost IP"); + ESP_LOGV(TAG, "Lost IP"); this->got_ipv4_address_ = false; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_SCAN_DONE) { const auto &it = data->data.sta_scan_done; - ESP_LOGV(TAG, "Event: WiFi Scan Done status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id); + ESP_LOGV(TAG, "Scan done: status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id); scan_result_.clear(); this->scan_done_ = true; @@ -772,28 +771,28 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) { - ESP_LOGV(TAG, "Event: WiFi AP start"); + ESP_LOGV(TAG, "AP start"); s_ap_started = true; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STOP) { - ESP_LOGV(TAG, "Event: WiFi AP stop"); + ESP_LOGV(TAG, "AP stop"); s_ap_started = false; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_PROBEREQRECVED) { const auto &it = data->data.ap_probe_req_rx; - ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STACONNECTED) { const auto &it = data->data.ap_staconnected; - ESP_LOGV(TAG, "Event: AP client connected MAC=%s", format_mac_addr(it.mac).c_str()); + ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_addr(it.mac).c_str()); } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STADISCONNECTED) { const auto &it = data->data.ap_stadisconnected; - ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s", format_mac_addr(it.mac).c_str()); + ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_addr(it.mac).c_str()); } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) { const auto &it = data->data.ip_ap_staipassigned; - ESP_LOGV(TAG, "Event: AP client assigned IP %s", format_ip4_addr(it.ip).c_str()); + ESP_LOGV(TAG, "AP client assigned IP %s", format_ip4_addr(it.ip).c_str()); } } @@ -873,7 +872,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { err = esp_netif_set_ip_info(s_ap_netif, &info); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_netif_set_ip_info failed! %d", err); + ESP_LOGE(TAG, "esp_netif_set_ip_info failed: %d", err); return false; } @@ -889,14 +888,14 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease)); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_netif_dhcps_option failed! %d", err); + ESP_LOGE(TAG, "esp_netif_dhcps_option failed: %d", err); return false; } err = esp_netif_dhcps_start(s_ap_netif); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_netif_dhcps_start failed! %d", err); + ESP_LOGE(TAG, "esp_netif_dhcps_start failed: %d", err); return false; } @@ -911,7 +910,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) { - ESP_LOGE(TAG, "AP SSID is too long"); + ESP_LOGE(TAG, "AP SSID too long"); return false; } memcpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); @@ -926,7 +925,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { } else { conf.ap.authmode = WIFI_AUTH_WPA2_PSK; if (ap.get_password().size() > sizeof(conf.ap.password)) { - ESP_LOGE(TAG, "AP password is too long"); + ESP_LOGE(TAG, "AP password too long"); return false; } memcpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), ap.get_password().size()); @@ -937,12 +936,12 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_wifi_set_config failed! %d", err); + ESP_LOGE(TAG, "esp_wifi_set_config failed: %d", err); return false; } if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { - ESP_LOGE(TAG, "wifi_ap_ip_config_ failed!"); + ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:"); return false; } diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index eb88ed81ad..0f7b688290 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -32,14 +32,14 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { return true; if (enable_sta && !current_sta) { - ESP_LOGV(TAG, "Enabling STA."); + ESP_LOGV(TAG, "Enabling STA"); } else if (!enable_sta && current_sta) { - ESP_LOGV(TAG, "Disabling STA."); + ESP_LOGV(TAG, "Disabling STA"); } if (enable_ap && !current_ap) { - ESP_LOGV(TAG, "Enabling AP."); + ESP_LOGV(TAG, "Enabling AP"); } else if (!enable_ap && current_ap) { - ESP_LOGV(TAG, "Disabling AP."); + ESP_LOGV(TAG, "Disabling AP"); } uint8_t mode = 0; @@ -124,7 +124,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { ap.get_channel().has_value() ? *ap.get_channel() : 0, ap.get_bssid().has_value() ? ap.get_bssid()->data() : NULL); if (status != WL_CONNECTED) { - ESP_LOGW(TAG, "esp_wifi_connect failed! %d", status); + ESP_LOGW(TAG, "esp_wifi_connect failed: %d", status); return false; } @@ -256,23 +256,23 @@ using esphome_wifi_event_info_t = arduino_event_info_t; void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_wifi_event_info_t info) { switch (event) { case ESPHOME_EVENT_ID_WIFI_READY: { - ESP_LOGV(TAG, "Event: WiFi ready"); + ESP_LOGV(TAG, "Ready"); break; } case ESPHOME_EVENT_ID_WIFI_SCAN_DONE: { auto it = info.wifi_scan_done; - ESP_LOGV(TAG, "Event: WiFi Scan Done status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id); + ESP_LOGV(TAG, "Scan done: status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id); this->wifi_scan_done_callback_(); break; } case ESPHOME_EVENT_ID_WIFI_STA_START: { - ESP_LOGV(TAG, "Event: WiFi STA start"); + ESP_LOGV(TAG, "STA start"); WiFi.setHostname(App.get_name().c_str()); break; } case ESPHOME_EVENT_ID_WIFI_STA_STOP: { - ESP_LOGV(TAG, "Event: WiFi STA stop"); + ESP_LOGV(TAG, "STA stop"); break; } case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: { @@ -280,7 +280,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ char buf[33]; memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; - ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, + ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); break; @@ -291,9 +291,9 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; if (it.reason == WIFI_REASON_NO_AP_FOUND) { - ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); + ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); } else { - ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); } @@ -310,8 +310,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: { auto it = info.wifi_sta_authmode_change; - ESP_LOGV(TAG, "Event: Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), - get_auth_mode_str(it.new_mode)); + ESP_LOGV(TAG, "Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode)); // Mitigate CVE-2020-12638 // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) { @@ -325,47 +324,47 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP: { // auto it = info.got_ip.ip_info; - ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(WiFi.localIP()).c_str(), + ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(WiFi.localIP()).c_str(), format_ip4_addr(WiFi.gatewayIP()).c_str()); s_sta_connecting = false; break; } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { // auto it = info.got_ip.ip_info; - ESP_LOGV(TAG, "Event: Got IPv6"); + ESP_LOGV(TAG, "Got IPv6"); break; } case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: { - ESP_LOGV(TAG, "Event: Lost IP"); + ESP_LOGV(TAG, "Lost IP"); break; } case ESPHOME_EVENT_ID_WIFI_AP_START: { - ESP_LOGV(TAG, "Event: WiFi AP start"); + ESP_LOGV(TAG, "AP start"); break; } case ESPHOME_EVENT_ID_WIFI_AP_STOP: { - ESP_LOGV(TAG, "Event: WiFi AP stop"); + ESP_LOGV(TAG, "AP stop"); break; } case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: { auto it = info.wifi_sta_connected; auto &mac = it.bssid; - ESP_LOGV(TAG, "Event: AP client connected MAC=%s", format_mac_addr(mac).c_str()); + ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_addr(mac).c_str()); break; } case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: { auto it = info.wifi_sta_disconnected; auto &mac = it.bssid; - ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s", format_mac_addr(mac).c_str()); + ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_addr(mac).c_str()); break; } case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: { - ESP_LOGV(TAG, "Event: AP client assigned IP"); + ESP_LOGV(TAG, "AP client assigned IP"); break; } case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: { auto it = info.wifi_ap_probereqrecved; - ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); break; } default: @@ -399,7 +398,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { // need to use WiFi because of WiFiScanClass allocations :( int16_t err = WiFi.scanNetworks(true, true, passive, 200); if (err != WIFI_SCAN_RUNNING) { - ESP_LOGV(TAG, "WiFi.scanNetworks failed! %d", err); + ESP_LOGV(TAG, "WiFi.scanNetworks failed: %d", err); return false; } @@ -447,7 +446,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return false; if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { - ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!"); + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 23fd766abe..bf15892cd5 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -134,7 +134,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { scan_options.scan_type = passive ? 1 : 0; int err = cyw43_wifi_scan(&cyw43_state, &scan_options, nullptr, &s_wifi_scan_result); if (err) { - ESP_LOGV(TAG, "cyw43_wifi_scan failed!"); + ESP_LOGV(TAG, "cyw43_wifi_scan failed"); } return err == 0; return true; @@ -162,7 +162,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { if (!this->wifi_mode_({}, true)) return false; if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { - ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!"); + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } @@ -209,7 +209,7 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { void WiFiComponent::wifi_loop_() { if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { this->scan_done_ = true; - ESP_LOGV(TAG, "Scan done!"); + ESP_LOGV(TAG, "Scan done"); } } diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index 150c7229f8..2612e4af8d 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -7,12 +7,12 @@ namespace wifi_info { static const char *const TAG = "wifi_info"; -void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo IPAddress", this); } -void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo Scan Results", this); } -void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo SSID", this); } -void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo BSSID", this); } -void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo Mac Address", this); } -void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo DNS Address", this); } +void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "IP Address", this); } +void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", this); } +void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); } +void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); } +void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "MAC Address", this); } +void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "DNS Address", this); } } // namespace wifi_info } // namespace esphome From 3e98cceb007179e695a22fe13d044b3bef402c7d Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Fri, 20 Jun 2025 05:33:46 -0500 Subject: [PATCH 080/293] [bh1750] Remove redundant platform name from logging (#9153) --- esphome/components/bh1750/bh1750.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index 4b51794907..267a728fdd 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -50,7 +50,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function< // turn on (after one-shot sensor automatically powers down) uint8_t turn_on = BH1750_COMMAND_POWER_ON; if (this->write(&turn_on, 1) != i2c::ERROR_OK) { - ESP_LOGW(TAG, "Turning on BH1750 failed"); + ESP_LOGW(TAG, "Power on failed"); f(NAN); return; } @@ -60,7 +60,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function< uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> 5) & 0b111); uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> 0) & 0b11111); if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) { - ESP_LOGW(TAG, "Setting measurement time for BH1750 failed"); + ESP_LOGW(TAG, "Set measurement time failed"); active_mtreg_ = 0; f(NAN); return; @@ -88,7 +88,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function< return; } if (this->write(&cmd, 1) != i2c::ERROR_OK) { - ESP_LOGW(TAG, "Starting measurement for BH1750 failed"); + ESP_LOGW(TAG, "Start measurement failed"); f(NAN); return; } @@ -99,7 +99,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function< this->set_timeout("read", meas_time, [this, mode, mtreg, f]() { uint16_t raw_value; if (this->read(reinterpret_cast(&raw_value), 2) != i2c::ERROR_OK) { - ESP_LOGW(TAG, "Reading BH1750 data failed"); + ESP_LOGW(TAG, "Read data failed"); f(NAN); return; } @@ -156,7 +156,7 @@ void BH1750Sensor::update() { this->publish_state(NAN); return; } - ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), val); + ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val); this->status_clear_warning(); this->publish_state(val); }); From b693b8ccb1a5fafdcbb33e96771cd0e761cc1b05 Mon Sep 17 00:00:00 2001 From: RoganDawes Date: Fri, 20 Jun 2025 14:03:15 +0200 Subject: [PATCH 081/293] [usb-host] Add support for USB Hubs (#9154) --- esphome/components/usb_host/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index b6ca779706..3204562dc8 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -19,6 +19,7 @@ USBClient = usb_host_ns.class_("USBClient", Component) CONF_DEVICES = "devices" CONF_VID = "vid" CONF_PID = "pid" +CONF_ENABLE_HUBS = "enable_hubs" def usb_device_schema(cls=USBClient, vid: int = None, pid: [int] = None) -> cv.Schema: @@ -42,6 +43,7 @@ CONFIG_SCHEMA = cv.All( cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(USBHost), + cv.Optional(CONF_ENABLE_HUBS, default=False): cv.boolean, cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()), } ), @@ -58,6 +60,8 @@ async def register_usb_client(config): async def to_code(config): add_idf_sdkconfig_option("CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE", 1024) + if config.get(CONF_ENABLE_HUBS): + add_idf_sdkconfig_option("CONFIG_USB_HOST_HUBS_SUPPORTED", True) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) for device in config.get(CONF_DEVICES) or (): From 169db9cc0acc4adbc61e77fec62828661ffe7b3c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 21 Jun 2025 07:55:08 +1000 Subject: [PATCH 082/293] [spi] Enable >6 devices with ESP-IDF (#9128) --- esphome/components/mipi_spi/display.py | 1 + esphome/components/spi/__init__.py | 16 +++-- esphome/components/spi/spi.cpp | 5 +- esphome/components/spi/spi.h | 12 +++- esphome/components/spi/spi_arduino.cpp | 8 +-- esphome/components/spi/spi_esp_idf.cpp | 61 ++++++++++++------- tests/components/spi_device/common.yaml | 8 +-- .../components/spi_device/test.esp32-idf.yaml | 5 ++ 8 files changed, 75 insertions(+), 41 deletions(-) diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index e9ed97a2a2..061257e859 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -472,3 +472,4 @@ async def to_code(config): cg.add(var.set_writer(lambda_)) await display.register_display(var, config) await spi.register_spi_device(var, config) + cg.add(var.set_write_only(True)) diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index ffb5e11f79..55a4b9c8f6 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -79,6 +79,7 @@ CONF_SPI_MODE = "spi_mode" CONF_FORCE_SW = "force_sw" CONF_INTERFACE = "interface" CONF_INTERFACE_INDEX = "interface_index" +CONF_RELEASE_DEVICE = "release_device" TYPE_SINGLE = "single" TYPE_QUAD = "quad" TYPE_OCTAL = "octal" @@ -378,6 +379,7 @@ def spi_device_schema( cv.Optional(CONF_SPI_MODE, default=default_mode): cv.enum( SPI_MODE_OPTIONS, upper=True ), + cv.Optional(CONF_RELEASE_DEVICE): cv.All(cv.boolean, cv.only_with_esp_idf), } if cs_pin_required: schema[cv.Required(CONF_CS_PIN)] = pins.gpio_output_pin_schema @@ -389,13 +391,15 @@ def spi_device_schema( async def register_spi_device(var, config): parent = await cg.get_variable(config[CONF_SPI_ID]) cg.add(var.set_spi_parent(parent)) - if CONF_CS_PIN in config: - pin = await cg.gpio_pin_expression(config[CONF_CS_PIN]) + if cs_pin := config.get(CONF_CS_PIN): + pin = await cg.gpio_pin_expression(cs_pin) cg.add(var.set_cs_pin(pin)) - if CONF_DATA_RATE in config: - cg.add(var.set_data_rate(config[CONF_DATA_RATE])) - if CONF_SPI_MODE in config: - cg.add(var.set_mode(config[CONF_SPI_MODE])) + if data_rate := config.get(CONF_DATA_RATE): + cg.add(var.set_data_rate(data_rate)) + if spi_mode := config.get(CONF_SPI_MODE): + cg.add(var.set_mode(spi_mode)) + if release_device := config.get(CONF_RELEASE_DEVICE): + cg.add(var.set_release_device(release_device)) def final_validate_device_schema(name: str, *, require_mosi: bool, require_miso: bool): diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index 76d9d8ae86..805a774ceb 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -16,12 +16,13 @@ bool SPIDelegate::is_ready() { return true; } GPIOPin *const NullPin::NULL_PIN = new NullPin(); // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) SPIDelegate *SPIComponent::register_device(SPIClient *device, SPIMode mode, SPIBitOrder bit_order, uint32_t data_rate, - GPIOPin *cs_pin) { + GPIOPin *cs_pin, bool release_device, bool write_only) { if (this->devices_.count(device) != 0) { ESP_LOGE(TAG, "Device already registered"); return this->devices_[device]; } - SPIDelegate *delegate = this->spi_bus_->get_delegate(data_rate, bit_order, mode, cs_pin); // NOLINT + SPIDelegate *delegate = + this->spi_bus_->get_delegate(data_rate, bit_order, mode, cs_pin, release_device, write_only); // NOLINT this->devices_[device] = delegate; return delegate; } diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index f96d3da251..5bc80350da 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -317,7 +317,8 @@ class SPIBus { SPIBus(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi) : clk_pin_(clk), sdo_pin_(sdo), sdi_pin_(sdi) {} - virtual SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) { + virtual SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin, + bool release_device, bool write_only) { return new SPIDelegateBitBash(data_rate, bit_order, mode, cs_pin, this->clk_pin_, this->sdo_pin_, this->sdi_pin_); } @@ -334,7 +335,7 @@ class SPIClient; class SPIComponent : public Component { public: SPIDelegate *register_device(SPIClient *device, SPIMode mode, SPIBitOrder bit_order, uint32_t data_rate, - GPIOPin *cs_pin); + GPIOPin *cs_pin, bool release_device, bool write_only); void unregister_device(SPIClient *device); void set_clk(GPIOPin *clk) { this->clk_pin_ = clk; } @@ -390,7 +391,8 @@ class SPIClient { virtual void spi_setup() { esph_log_d("spi_device", "mode %u, data_rate %ukHz", (unsigned) this->mode_, (unsigned) (this->data_rate_ / 1000)); - this->delegate_ = this->parent_->register_device(this, this->mode_, this->bit_order_, this->data_rate_, this->cs_); + this->delegate_ = this->parent_->register_device(this, this->mode_, this->bit_order_, this->data_rate_, this->cs_, + this->release_device_, this->write_only_); } virtual void spi_teardown() { @@ -399,6 +401,8 @@ class SPIClient { } bool spi_is_ready() { return this->delegate_->is_ready(); } + void set_release_device(bool release) { this->release_device_ = release; } + void set_write_only(bool write_only) { this->write_only_ = write_only; } protected: SPIBitOrder bit_order_{BIT_ORDER_MSB_FIRST}; @@ -406,6 +410,8 @@ class SPIClient { uint32_t data_rate_{1000000}; SPIComponent *parent_{nullptr}; GPIOPin *cs_{nullptr}; + bool release_device_{false}; + bool write_only_{false}; SPIDelegate *delegate_{SPIDelegate::NULL_DELEGATE}; }; diff --git a/esphome/components/spi/spi_arduino.cpp b/esphome/components/spi/spi_arduino.cpp index 432f7cf2cd..a34e3c3c82 100644 --- a/esphome/components/spi/spi_arduino.cpp +++ b/esphome/components/spi/spi_arduino.cpp @@ -43,10 +43,7 @@ class SPIDelegateHw : public SPIDelegate { return; } #ifdef USE_RP2040 - // avoid overwriting the supplied buffer. Use vector for automatic deallocation - auto rxbuf = std::vector(length); - memcpy(rxbuf.data(), ptr, length); - this->channel_->transfer((void *) rxbuf.data(), length); + this->channel_->transfer(ptr, nullptr, length); #elif defined(USE_ESP8266) // ESP8266 SPI library requires the pointer to be word aligned, but the data may not be // so we need to copy the data to a temporary buffer @@ -89,7 +86,8 @@ class SPIBusHw : public SPIBus { #endif } - SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) override { + SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin, + bool release_device, bool write_only) override { return new SPIDelegateHw(this->channel_, data_rate, bit_order, mode, cs_pin); } diff --git a/esphome/components/spi/spi_esp_idf.cpp b/esphome/components/spi/spi_esp_idf.cpp index a78da2cd9a..549f516eb1 100644 --- a/esphome/components/spi/spi_esp_idf.cpp +++ b/esphome/components/spi/spi_esp_idf.cpp @@ -11,34 +11,26 @@ static const size_t MAX_TRANSFER_SIZE = 4092; // dictated by ESP-IDF API. class SPIDelegateHw : public SPIDelegate { public: SPIDelegateHw(SPIInterface channel, uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin, - bool write_only) - : SPIDelegate(data_rate, bit_order, mode, cs_pin), channel_(channel), write_only_(write_only) { - spi_device_interface_config_t config = {}; - config.mode = static_cast(mode); - config.clock_speed_hz = static_cast(data_rate); - config.spics_io_num = -1; - config.flags = 0; - config.queue_size = 1; - config.pre_cb = nullptr; - config.post_cb = nullptr; - if (bit_order == BIT_ORDER_LSB_FIRST) - config.flags |= SPI_DEVICE_BIT_LSBFIRST; - if (write_only) - config.flags |= SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY; - esp_err_t const err = spi_bus_add_device(channel, &config, &this->handle_); - if (err != ESP_OK) - ESP_LOGE(TAG, "Add device failed - err %X", err); + bool release_device, bool write_only) + : SPIDelegate(data_rate, bit_order, mode, cs_pin), + channel_(channel), + release_device_(release_device), + write_only_(write_only) { + if (!this->release_device_) + add_device_(); } bool is_ready() override { return this->handle_ != nullptr; } void begin_transaction() override { + if (this->release_device_) + this->add_device_(); if (this->is_ready()) { if (spi_device_acquire_bus(this->handle_, portMAX_DELAY) != ESP_OK) ESP_LOGE(TAG, "Failed to acquire SPI bus"); SPIDelegate::begin_transaction(); } else { - ESP_LOGW(TAG, "spi_setup called before initialisation"); + ESP_LOGW(TAG, "SPI device not ready, cannot begin transaction"); } } @@ -46,6 +38,10 @@ class SPIDelegateHw : public SPIDelegate { if (this->is_ready()) { SPIDelegate::end_transaction(); spi_device_release_bus(this->handle_); + if (this->release_device_) { + spi_bus_remove_device(this->handle_); + this->handle_ = nullptr; // reset handle to indicate no device is registered + } } } @@ -189,8 +185,30 @@ class SPIDelegateHw : public SPIDelegate { void read_array(uint8_t *ptr, size_t length) override { this->transfer(nullptr, ptr, length); } protected: + bool add_device_() { + spi_device_interface_config_t config = {}; + config.mode = static_cast(this->mode_); + config.clock_speed_hz = static_cast(this->data_rate_); + config.spics_io_num = -1; + config.flags = 0; + config.queue_size = 1; + config.pre_cb = nullptr; + config.post_cb = nullptr; + if (this->bit_order_ == BIT_ORDER_LSB_FIRST) + config.flags |= SPI_DEVICE_BIT_LSBFIRST; + if (this->write_only_) + config.flags |= SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY; + esp_err_t const err = spi_bus_add_device(this->channel_, &config, &this->handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Add device failed - err %X", err); + return false; + } + return true; + } + SPIInterface channel_{}; spi_device_handle_t handle_{}; + bool release_device_{false}; bool write_only_{false}; }; @@ -231,9 +249,10 @@ class SPIBusHw : public SPIBus { ESP_LOGE(TAG, "Bus init failed - err %X", err); } - SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) override { - return new SPIDelegateHw(this->channel_, data_rate, bit_order, mode, cs_pin, - Utility::get_pin_no(this->sdi_pin_) == -1); + SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin, + bool release_device, bool write_only) override { + return new SPIDelegateHw(this->channel_, data_rate, bit_order, mode, cs_pin, release_device, + write_only || Utility::get_pin_no(this->sdi_pin_) == -1); } protected: diff --git a/tests/components/spi_device/common.yaml b/tests/components/spi_device/common.yaml index 636d82202b..0f6a5038fb 100644 --- a/tests/components/spi_device/common.yaml +++ b/tests/components/spi_device/common.yaml @@ -5,7 +5,7 @@ spi: miso_pin: ${miso_pin} spi_device: - id: spi_device_test - data_rate: 2MHz - spi_mode: 3 - bit_order: lsb_first + - id: spi_device_test + data_rate: 2MHz + spi_mode: 3 + bit_order: lsb_first diff --git a/tests/components/spi_device/test.esp32-idf.yaml b/tests/components/spi_device/test.esp32-idf.yaml index 448e54fea6..c4989cccbf 100644 --- a/tests/components/spi_device/test.esp32-idf.yaml +++ b/tests/components/spi_device/test.esp32-idf.yaml @@ -4,3 +4,8 @@ substitutions: miso_pin: GPIO15 <<: !include common.yaml +spi_device: + - id: spi_device_test + release_device: true + data_rate: 1MHz + spi_mode: 0 From 4ef0264ed3d1632b35677dfa59f5dce1e57da428 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 21 Jun 2025 19:32:24 +1200 Subject: [PATCH 083/293] Clean up RAMAllocators in light related code (#9142) --- esphome/components/beken_spi_led_strip/led_strip.cpp | 6 ++++-- .../m5stack_8angle/light/m5stack_8angle_light.cpp | 2 +- esphome/components/rp2040_pio_led_strip/led_strip.cpp | 4 ++-- esphome/components/spi_led_strip/spi_led_strip.cpp | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/esphome/components/beken_spi_led_strip/led_strip.cpp b/esphome/components/beken_spi_led_strip/led_strip.cpp index d4585d7d36..17b2dd1808 100644 --- a/esphome/components/beken_spi_led_strip/led_strip.cpp +++ b/esphome/components/beken_spi_led_strip/led_strip.cpp @@ -7,11 +7,13 @@ extern "C" { #include "rtos_pub.h" -#include "spi.h" +// rtos_pub.h must be included before the rest of the includes + #include "arm_arch.h" #include "general_dma_pub.h" #include "gpio_pub.h" #include "icu_pub.h" +#include "spi.h" #undef SPI_DAT #undef SPI_BASE }; @@ -124,7 +126,7 @@ void BekenSPILEDStripLightOutput::setup() { size_t buffer_size = this->get_buffer_size_(); size_t dma_buffer_size = (buffer_size * 8) + (2 * 64); - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->buf_ = allocator.allocate(buffer_size); if (this->buf_ == nullptr) { ESP_LOGE(TAG, "Cannot allocate LED buffer!"); diff --git a/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp b/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp index 95fd8cb98f..0e7b902919 100644 --- a/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp +++ b/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp @@ -8,7 +8,7 @@ namespace m5stack_8angle { static const char *const TAG = "m5stack_8angle.light"; void M5Stack8AngleLightOutput::setup() { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->buf_ = allocator.allocate(M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED); if (this->buf_ == nullptr) { ESP_LOGE(TAG, "Failed to allocate buffer of size %u", M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED); diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index a6ff037d88..42f7e9cf52 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -9,8 +9,8 @@ #include #include #include -#include #include +#include namespace esphome { namespace rp2040_pio_led_strip { @@ -44,7 +44,7 @@ void RP2040PIOLEDStripLightOutput::setup() { size_t buffer_size = this->get_buffer_size_(); - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->buf_ = allocator.allocate(buffer_size); if (this->buf_ == nullptr) { ESP_LOGE(TAG, "Failed to allocate buffer of size %u", buffer_size); diff --git a/esphome/components/spi_led_strip/spi_led_strip.cpp b/esphome/components/spi_led_strip/spi_led_strip.cpp index 46243c0686..85c10ee87d 100644 --- a/esphome/components/spi_led_strip/spi_led_strip.cpp +++ b/esphome/components/spi_led_strip/spi_led_strip.cpp @@ -5,7 +5,7 @@ namespace spi_led_strip { SpiLedStrip::SpiLedStrip(uint16_t num_leds) { this->num_leds_ = num_leds; - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->buffer_size_ = num_leds * 4 + 8; this->buf_ = allocator.allocate(this->buffer_size_); if (this->buf_ == nullptr) { From a6c20853ca1c9a47f7220fb7615c21e719533ced Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Sat, 21 Jun 2025 11:26:14 +0200 Subject: [PATCH 084/293] [nextion] Extract common `upload_end_` function to shared file (#9155) --- esphome/components/nextion/nextion_upload.cpp | 36 +++++++++++++++++++ .../nextion/nextion_upload_arduino.cpp | 25 ------------- .../components/nextion/nextion_upload_idf.cpp | 24 ------------- 3 files changed, 36 insertions(+), 49 deletions(-) create mode 100644 esphome/components/nextion/nextion_upload.cpp diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp new file mode 100644 index 0000000000..6a54abfed4 --- /dev/null +++ b/esphome/components/nextion/nextion_upload.cpp @@ -0,0 +1,36 @@ +#include "nextion.h" + +#ifdef USE_NEXTION_TFT_UPLOAD + +#include "esphome/core/application.h" + +namespace esphome { +namespace nextion { +static const char *const TAG = "nextion.upload"; + +bool Nextion::upload_end_(bool successful) { + if (successful) { + ESP_LOGD(TAG, "Upload successful"); + delay(1500); // NOLINT + App.safe_reboot(); + } else { + ESP_LOGE(TAG, "Upload failed"); + + this->is_updating_ = false; + this->ignore_is_setup_ = false; + + uint32_t baud_rate = this->parent_->get_baud_rate(); + if (baud_rate != this->original_baud_rate_) { + ESP_LOGD(TAG, "Baud: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_); + this->parent_->set_baud_rate(this->original_baud_rate_); + this->parent_->load_settings(); + } + } + + return successful; +} + +} // namespace nextion +} // namespace esphome + +#endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index c2d0f2a22d..6cd03118d2 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -335,31 +335,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return upload_end_(true); } -bool Nextion::upload_end_(bool successful) { - ESP_LOGD(TAG, "TFT upload done: %s", YESNO(successful)); - - if (successful) { - ESP_LOGD(TAG, "Restart"); - delay(1500); // NOLINT - App.safe_reboot(); - delay(1500); // NOLINT - } else { - ESP_LOGE(TAG, "TFT upload failed"); - - this->is_updating_ = false; - this->ignore_is_setup_ = false; - - uint32_t baud_rate = this->parent_->get_baud_rate(); - if (baud_rate != this->original_baud_rate_) { - ESP_LOGD(TAG, "Baud back: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_); - this->parent_->set_baud_rate(this->original_baud_rate_); - this->parent_->load_settings(); - } - } - - return successful; -} - #ifdef USE_ESP8266 WiFiClient *Nextion::get_wifi_client_() { if (this->tft_url_.compare(0, 6, "https:") == 0) { diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp index 43b80f7761..14ce46d0a0 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -335,30 +335,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return this->upload_end_(true); } -bool Nextion::upload_end_(bool successful) { - ESP_LOGD(TAG, "TFT upload done: %s", YESNO(successful)); - - if (successful) { - ESP_LOGD(TAG, "Restart"); - delay(1500); // NOLINT - App.safe_reboot(); - } else { - ESP_LOGE(TAG, "TFT upload failed"); - - this->is_updating_ = false; - this->ignore_is_setup_ = false; - - uint32_t baud_rate = this->parent_->get_baud_rate(); - if (baud_rate != this->original_baud_rate_) { - ESP_LOGD(TAG, "Baud back: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_); - this->parent_->set_baud_rate(this->original_baud_rate_); - this->parent_->load_settings(); - } - } - - return successful; -} - } // namespace nextion } // namespace esphome From ac9c608542829974e09b6766583f84811955bae8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Jun 2025 18:13:07 +0200 Subject: [PATCH 085/293] Bump esptool from 4.8.1 to 4.9.0 (#9158) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 76a58bf622..4932fb48f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ tzlocal==5.3.1 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile -esptool==4.8.1 +esptool==4.9.0 click==8.1.7 esphome-dashboard==20250514.0 aioesphomeapi==32.2.4 From c81dbf9d59b8b5a04cf76d4cdd2b01c207c75387 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Sun, 22 Jun 2025 12:09:38 +0200 Subject: [PATCH 086/293] Improve on C++17 (#9170) --- esphome/components/host/__init__.py | 2 +- esphome/cpp_generator.py | 6 ++++++ platformio.ini | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index e275adafa9..b59d8ebd03 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -41,6 +41,6 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): cg.add_build_flag("-DUSE_HOST") cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) - cg.add_build_flag("-std=c++17") + cg.add_build_flag("-std=gnu++17") cg.add_define("ESPHOME_BOARD", "host") cg.add_platformio_option("platform", "platformio/native") diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 4641f69bdd..2a7b7fe057 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -616,6 +616,12 @@ def add_build_unflag(build_unflag: str) -> None: def set_cpp_standard(standard: str) -> None: """Set C++ standard with compiler flag `-std={standard}`.""" CORE.add_build_unflag("-std=gnu++11") + CORE.add_build_unflag("-std=gnu++14") + CORE.add_build_unflag("-std=gnu++20") + CORE.add_build_unflag("-std=gnu++23") + CORE.add_build_unflag("-std=gnu++2a") + CORE.add_build_unflag("-std=gnu++2b") + CORE.add_build_unflag("-std=gnu++2c") CORE.add_build_flag(f"-std={standard}") diff --git a/platformio.ini b/platformio.ini index f67226d657..6da9fc1338 100644 --- a/platformio.ini +++ b/platformio.ini @@ -50,6 +50,12 @@ build_flags = -std=gnu++17 build_unflags = -std=gnu++11 + -std=gnu++14 + -std=gnu++20 + -std=gnu++23 + -std=gnu++2a + -std=gnu++2b + -std=gnu++2c src_filter = +<./> +<../tests/dummy_main.cpp> From cbfd904b9f9fb178dfe119d2d1e287ee0273be8f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 11:00:42 +0000 Subject: [PATCH 087/293] Bump aioesphomeapi from 32.2.4 to 33.1.0 (#9173) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4932fb48f5..01bbfa91c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.9.0 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==32.2.4 +aioesphomeapi==33.1.0 zeroconf==0.147.0 puremagic==1.29 ruamel.yaml==0.18.14 # dashboard_import From 788803d5884185b2a8b8d078b7579e2ff0016396 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 19:05:54 +0000 Subject: [PATCH 088/293] Bump flake8 from 7.2.0 to 7.3.0 (#9172) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 634c474571..96efee7020 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: # Run the formatter. - id: ruff-format - repo: https://github.com/PyCQA/flake8 - rev: 7.2.0 + rev: 7.3.0 hooks: - id: flake8 additional_dependencies: diff --git a/requirements_test.txt b/requirements_test.txt index 9263d165ac..89aba702b9 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,5 +1,5 @@ pylint==3.3.7 -flake8==7.2.0 # also change in .pre-commit-config.yaml when updating +flake8==7.3.0 # also change in .pre-commit-config.yaml when updating ruff==0.12.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pre-commit From 7ab9083d778d47b7a88db8ec1d0c56c13027677b Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Sun, 22 Jun 2025 22:56:50 +0200 Subject: [PATCH 089/293] [nextion] Revert to `millis()` on `recv_ret_string_` (#9168) --- esphome/components/nextion/nextion.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 4f08fcb393..042a595ff8 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -963,13 +963,12 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool uint16_t ret = 0; uint8_t c = 0; uint8_t nr_of_ff_bytes = 0; - uint64_t start; bool exit_flag = false; bool ff_flag = false; - start = App.get_loop_component_start_time(); + const uint32_t start = millis(); - while ((timeout == 0 && this->available()) || App.get_loop_component_start_time() - start <= timeout) { + while ((timeout == 0 && this->available()) || millis() - start <= timeout) { if (!this->available()) { App.feed_wdt(); delay(1); @@ -1038,7 +1037,7 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) { nextion_queue->component = new nextion::NextionComponentBase; nextion_queue->component->set_variable_name(variable_name); - nextion_queue->queue_time = App.get_loop_component_start_time(); + nextion_queue->queue_time = millis(); this->nextion_queue_.push_back(nextion_queue); From dc5cbd4df8e658c48218f93e43dd1ecfc4d14d61 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 23 Jun 2025 09:54:49 +1200 Subject: [PATCH 090/293] [const] Move ``CONF_DEVICES`` to ``const.py`` (#9179) --- esphome/components/usb_host/__init__.py | 3 +-- esphome/const.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index 3204562dc8..0fe3310127 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -6,7 +6,7 @@ from esphome.components.esp32 import ( only_on_variant, ) import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_DEVICES, CONF_ID from esphome.cpp_types import Component AUTO_LOAD = ["bytebuffer"] @@ -16,7 +16,6 @@ usb_host_ns = cg.esphome_ns.namespace("usb_host") USBHost = usb_host_ns.class_("USBHost", Component) USBClient = usb_host_ns.class_("USBClient", Component) -CONF_DEVICES = "devices" CONF_VID = "vid" CONF_PID = "pid" CONF_ENABLE_HUBS = "enable_hubs" diff --git a/esphome/const.py b/esphome/const.py index 69d75c81ce..e61af6c5b5 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -217,6 +217,7 @@ CONF_DEST = "dest" CONF_DEVICE = "device" CONF_DEVICE_CLASS = "device_class" CONF_DEVICE_FACTOR = "device_factor" +CONF_DEVICES = "devices" CONF_DIELECTRIC_CONSTANT = "dielectric_constant" CONF_DIMENSIONS = "dimensions" CONF_DIO_PIN = "dio_pin" From 59889a6286eed421c33880a1f931c0bc72733a95 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 04:06:02 +0200 Subject: [PATCH 091/293] Reduce Logger memory usage by optimizing variable sizes (#9161) --- esphome/components/logger/__init__.py | 4 +- esphome/components/logger/logger.cpp | 20 ++--- esphome/components/logger/logger.h | 111 ++++++++++++++------------ esphome/core/log.cpp | 4 +- 4 files changed, 76 insertions(+), 63 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 26516e1506..af62d8a73f 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -184,7 +184,9 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(Logger), cv.Optional(CONF_BAUD_RATE, default=115200): cv.positive_int, - cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.validate_bytes, + cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.All( + cv.validate_bytes, cv.int_range(min=160, max=65535) + ), cv.Optional(CONF_DEASSERT_RTS_DTR, default=False): cv.boolean, cv.SplitDefault( CONF_TASK_LOG_BUFFER_SIZE, diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 28a66b23b7..b42496af66 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -24,7 +24,7 @@ static const char *const TAG = "logger"; // - Messages are serialized through main loop for proper console output // - Fallback to emergency console logging only if ring buffer is full // - WITHOUT task log buffer: Only emergency console output, no callbacks -void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT +void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT if (level > this->level_for(tag)) return; @@ -46,8 +46,8 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * bool message_sent = false; #ifdef USE_ESPHOME_TASK_LOG_BUFFER // For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered - message_sent = this->log_buffer_->send_message_thread_safe(static_cast(level), tag, - static_cast(line), current_task, format, args); + message_sent = + this->log_buffer_->send_message_thread_safe(level, tag, static_cast(line), current_task, format, args); #endif // USE_ESPHOME_TASK_LOG_BUFFER // Emergency console logging for non-main tasks when ring buffer is full or disabled @@ -58,7 +58,7 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * // Maximum size for console log messages (includes null terminator) static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144; char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety - int buffer_at = 0; // Initialize buffer position + uint16_t buffer_at = 0; // Initialize buffer position this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); this->write_msg_(console_buffer); @@ -69,7 +69,7 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * } #else // Implementation for all other platforms -void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT +void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT if (level > this->level_for(tag) || global_recursion_guard_) return; @@ -85,7 +85,7 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * #ifdef USE_STORE_LOG_STR_IN_FLASH // Implementation for ESP8266 with flash string support. // Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266. -void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, +void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, va_list args) { // NOLINT if (level > this->level_for(tag) || global_recursion_guard_) return; @@ -122,7 +122,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr } #endif // USE_STORE_LOG_STR_IN_FLASH -inline int Logger::level_for(const char *tag) { +inline uint8_t Logger::level_for(const char *tag) { auto it = this->log_levels_.find(tag); if (it != this->log_levels_.end()) return it->second; @@ -195,13 +195,13 @@ void Logger::loop() { #endif void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } -void Logger::set_log_level(const std::string &tag, int log_level) { this->log_levels_[tag] = log_level; } +void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) UARTSelection Logger::get_uart() const { return this->uart_; } #endif -void Logger::add_on_log_callback(std::function &&callback) { +void Logger::add_on_log_callback(std::function &&callback) { this->log_callback_.add(std::move(callback)); } float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } @@ -230,7 +230,7 @@ void Logger::dump_config() { } } -void Logger::set_log_level(int level) { +void Logger::set_log_level(uint8_t level) { if (level > ESPHOME_LOG_LEVEL) { level = ESPHOME_LOG_LEVEL; ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]); diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 9f09208b66..ea82764393 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -61,7 +61,7 @@ static const char *const LOG_LEVEL_LETTERS[] = { * * Advanced configuration (pin selection, etc) is not supported. */ -enum UARTSelection { +enum UARTSelection : uint8_t { #ifdef USE_LIBRETINY UART_SELECTION_DEFAULT = 0, UART_SELECTION_UART0, @@ -129,10 +129,10 @@ class Logger : public Component { #endif /// Set the default log level for this logger. - void set_log_level(int level); + void set_log_level(uint8_t level); /// Set the log level of the specified tag. - void set_log_level(const std::string &tag, int log_level); - int get_log_level() { return this->current_level_; } + void set_log_level(const std::string &tag, uint8_t log_level); + uint8_t get_log_level() { return this->current_level_; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -140,19 +140,20 @@ class Logger : public Component { void pre_setup(); void dump_config() override; - inline int level_for(const char *tag); + inline uint8_t level_for(const char *tag); /// Register a callback that will be called for every log message sent - void add_on_log_callback(std::function &&callback); + void add_on_log_callback(std::function &&callback); // add a listener for log level changes - void add_listener(std::function &&callback) { this->level_callback_.add(std::move(callback)); } + void add_listener(std::function &&callback) { this->level_callback_.add(std::move(callback)); } float get_setup_priority() const override; - void log_vprintf_(int level, const char *tag, int line, const char *format, va_list args); // NOLINT + void log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args); // NOLINT #ifdef USE_STORE_LOG_STR_IN_FLASH - void log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args); // NOLINT + void log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, + va_list args); // NOLINT #endif protected: @@ -160,8 +161,9 @@ class Logger : public Component { // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator // It's the caller's responsibility to initialize buffer_at (typically to 0) - inline void HOT format_log_to_buffer_with_terminator_(int level, const char *tag, int line, const char *format, - va_list args, char *buffer, int *buffer_at, int buffer_size) { + inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, + va_list args, char *buffer, uint16_t *buffer_at, + uint16_t buffer_size) { #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); #else @@ -180,7 +182,7 @@ class Logger : public Component { } // Helper to format and send a log message to both console and callbacks - inline void HOT log_message_to_buffer_and_send_(int level, const char *tag, int line, const char *format, + inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // Format to tx_buffer and prepare for output this->tx_buffer_at_ = 0; // Initialize buffer position @@ -194,11 +196,12 @@ class Logger : public Component { } // Write the body of the log message to the buffer - inline void write_body_to_buffer_(const char *value, size_t length, char *buffer, int *buffer_at, int buffer_size) { + inline void write_body_to_buffer_(const char *value, size_t length, char *buffer, uint16_t *buffer_at, + uint16_t buffer_size) { // Calculate available space - const int available = buffer_size - *buffer_at; - if (available <= 0) + if (*buffer_at >= buffer_size) return; + const uint16_t available = buffer_size - *buffer_at; // Determine copy length (minimum of remaining capacity and string length) const size_t copy_len = (length < static_cast(available)) ? length : available; @@ -211,7 +214,7 @@ class Logger : public Component { } // Format string to explicit buffer with varargs - inline void printf_to_buffer_(char *buffer, int *buffer_at, int buffer_size, const char *format, ...) { + inline void printf_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, ...) { va_list arg; va_start(arg, format); this->format_body_to_buffer_(buffer, buffer_at, buffer_size, format, arg); @@ -222,41 +225,50 @@ class Logger : public Component { const char *get_uart_selection_(); #endif + // Group 4-byte aligned members first uint32_t baud_rate_; char *tx_buffer_{nullptr}; - int tx_buffer_at_{0}; - int tx_buffer_size_{0}; +#ifdef USE_ARDUINO + Stream *hw_serial_{nullptr}; +#endif +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + void *main_task_ = nullptr; // Only used for thread name identification +#endif +#ifdef USE_ESP32 + // Task-specific recursion guards: + // - Main task uses a dedicated member variable for efficiency + // - Other tasks use pthread TLS with a dynamically created key via pthread_key_create + pthread_key_t log_recursion_key_; // 4 bytes +#endif +#ifdef USE_ESP_IDF + uart_port_t uart_num_; // 4 bytes (enum defaults to int size) +#endif + + // Large objects (internally aligned) + std::map log_levels_{}; + CallbackManager log_callback_{}; + CallbackManager level_callback_{}; +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer +#endif + + // Group smaller types together at the end + uint16_t tx_buffer_at_{0}; + uint16_t tx_buffer_size_{0}; + uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) UARTSelection uart_{UART_SELECTION_UART0}; #endif #ifdef USE_LIBRETINY UARTSelection uart_{UART_SELECTION_DEFAULT}; #endif -#ifdef USE_ARDUINO - Stream *hw_serial_{nullptr}; -#endif -#ifdef USE_ESP_IDF - uart_port_t uart_num_; -#endif - std::map log_levels_{}; - CallbackManager log_callback_{}; - int current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; -#ifdef USE_ESPHOME_TASK_LOG_BUFFER - std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer -#endif #ifdef USE_ESP32 - // Task-specific recursion guards: - // - Main task uses a dedicated member variable for efficiency - // - Other tasks use pthread TLS with a dynamically created key via pthread_key_create bool main_task_recursion_guard_{false}; - pthread_key_t log_recursion_key_; #else bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms #endif - CallbackManager level_callback_{}; #if defined(USE_ESP32) || defined(USE_LIBRETINY) - void *main_task_ = nullptr; // Only used for thread name identification const char *HOT get_thread_name_() { TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); if (current_task == main_task_) { @@ -297,11 +309,10 @@ class Logger : public Component { } #endif - inline void HOT write_header_to_buffer_(int level, const char *tag, int line, const char *thread_name, char *buffer, - int *buffer_at, int buffer_size) { + inline void HOT write_header_to_buffer_(uint8_t level, const char *tag, int line, const char *thread_name, + char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { // Format header - if (level < 0) - level = 0; + // uint8_t level is already bounded 0-255, just ensure it's <= 7 if (level > 7) level = 7; @@ -320,12 +331,12 @@ class Logger : public Component { this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]: ", color, letter, tag, line); } - inline void HOT format_body_to_buffer_(char *buffer, int *buffer_at, int buffer_size, const char *format, + inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, va_list args) { // Get remaining capacity in the buffer - const int remaining = buffer_size - *buffer_at; - if (remaining <= 0) + if (*buffer_at >= buffer_size) return; + const uint16_t remaining = buffer_size - *buffer_at; const int ret = vsnprintf(buffer + *buffer_at, remaining, format, args); @@ -334,7 +345,7 @@ class Logger : public Component { } // Update buffer_at with the formatted length (handle truncation) - int formatted_len = (ret >= remaining) ? remaining : ret; + uint16_t formatted_len = (ret >= remaining) ? remaining : ret; *buffer_at += formatted_len; // Remove all trailing newlines right after formatting @@ -343,18 +354,18 @@ class Logger : public Component { } } - inline void HOT write_footer_to_buffer_(char *buffer, int *buffer_at, int buffer_size) { - static const int RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR); + inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { + static const uint16_t RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR); this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); } }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class LoggerMessageTrigger : public Trigger { +class LoggerMessageTrigger : public Trigger { public: - explicit LoggerMessageTrigger(Logger *parent, int level) { + explicit LoggerMessageTrigger(Logger *parent, uint8_t level) { this->level_ = level; - parent->add_on_log_callback([this](int level, const char *tag, const char *message) { + parent->add_on_log_callback([this](uint8_t level, const char *tag, const char *message) { if (level <= this->level_) { this->trigger(level, tag, message); } @@ -362,7 +373,7 @@ class LoggerMessageTrigger : public Trigger { } protected: - int level_; + uint8_t level_; }; } // namespace logger diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp index 424154d253..909319dd28 100644 --- a/esphome/core/log.cpp +++ b/esphome/core/log.cpp @@ -29,7 +29,7 @@ void HOT esp_log_vprintf_(int level, const char *tag, int line, const char *form if (log == nullptr) return; - log->log_vprintf_(level, tag, line, format, args); + log->log_vprintf_(static_cast(level), tag, line, format, args); #endif } @@ -41,7 +41,7 @@ void HOT esp_log_vprintf_(int level, const char *tag, int line, const __FlashStr if (log == nullptr) return; - log->log_vprintf_(level, tag, line, format, args); + log->log_vprintf_(static_cast(level), tag, line, format, args); #endif } #endif From 04f592ba6d5e6d16b0978de04814abd3882f3666 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 04:07:53 +0200 Subject: [PATCH 092/293] Fix slow noise handshake by reading multiple messages per loop (#9130) --- esphome/components/api/api_connection.cpp | 60 ++++++++++++--------- esphome/components/api/api_frame_helper.cpp | 17 ++++-- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ca5689bdf6..ef791d462c 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -28,6 +28,12 @@ namespace esphome { namespace api { +// Read a maximum of 5 messages per loop iteration to prevent starving other components. +// This is a balance between API responsiveness and allowing other components to run. +// Since each message could contain multiple protobuf messages when using packet batching, +// this limits the number of messages processed, not the number of TCP packets. +static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5; + static const char *const TAG = "api.connection"; static const int ESP32_CAMERA_STOP_STREAM = 5000; @@ -109,33 +115,38 @@ void APIConnection::loop() { return; } + const uint32_t now = App.get_loop_component_start_time(); // Check if socket has data ready before attempting to read if (this->helper_->is_socket_ready()) { - ReadPacketBuffer buffer; - err = this->helper_->read_packet(&buffer); - if (err == APIError::WOULD_BLOCK) { - // pass - } else if (err != APIError::OK) { - on_fatal_error(); - if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); - } else if (err == APIError::CONNECTION_CLOSED) { - ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str()); - } else { - ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(), - api_error_to_str(err), errno); - } - return; - } else { - this->last_traffic_ = App.get_loop_component_start_time(); - // read a packet - if (buffer.data_len > 0) { - this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); - } else { - this->read_message(0, buffer.type, nullptr); - } - if (this->remove_) + // Read up to MAX_MESSAGES_PER_LOOP messages per loop to improve throughput + for (uint8_t message_count = 0; message_count < MAX_MESSAGES_PER_LOOP; message_count++) { + ReadPacketBuffer buffer; + err = this->helper_->read_packet(&buffer); + if (err == APIError::WOULD_BLOCK) { + // No more data available + break; + } else if (err != APIError::OK) { + on_fatal_error(); + if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { + ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); + } else if (err == APIError::CONNECTION_CLOSED) { + ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str()); + } else { + ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(), + api_error_to_str(err), errno); + } return; + } else { + this->last_traffic_ = now; + // read a packet + if (buffer.data_len > 0) { + this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); + } else { + this->read_message(0, buffer.type, nullptr); + } + if (this->remove_) + return; + } } } @@ -152,7 +163,6 @@ void APIConnection::loop() { static uint8_t max_ping_retries = 60; static uint16_t ping_retry_interval = 1000; - const uint32_t now = App.get_loop_component_start_time(); if (this->sent_ping_) { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) { diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index e0eb94836d..ff660f439e 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -274,12 +274,21 @@ APIError APINoiseFrameHelper::init() { } /// Run through handshake messages (if in that phase) APIError APINoiseFrameHelper::loop() { - APIError err = state_action_(); - if (err != APIError::OK && err != APIError::WOULD_BLOCK) { - return err; + // During handshake phase, process as many actions as possible until we can't progress + // socket_->ready() stays true until next main loop, but state_action() will return + // WOULD_BLOCK when no more data is available to read + while (state_ != State::DATA && this->socket_->ready()) { + APIError err = state_action_(); + if (err != APIError::OK && err != APIError::WOULD_BLOCK) { + return err; + } + if (err == APIError::WOULD_BLOCK) { + break; + } } + if (!this->tx_buf_.empty()) { - err = try_send_tx_buf_(); + APIError err = try_send_tx_buf_(); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { return err; } From 7fc5bfd787794431cc386c6f1be09b42fef51d0e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 04:09:34 +0200 Subject: [PATCH 093/293] Reduce RAM usage for scheduled tasks (#9180) --- esphome/core/scheduler.cpp | 4 ++++ esphome/core/scheduler.h | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index eed222c974..8144435163 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -319,13 +319,17 @@ bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, return ret; } uint64_t Scheduler::millis_() { + // Get the current 32-bit millis value const uint32_t now = millis(); + // Check for rollover by comparing with last value if (now < this->last_millis_) { + // Detected rollover (happens every ~49.7 days) this->millis_major_++; ESP_LOGD(TAG, "Incrementing scheduler major at %" PRIu64 "ms", now + (static_cast(this->millis_major_) << 32)); } this->last_millis_ = now; + // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time return now + (static_cast(this->millis_major_) << 32); } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 872a8bd6f6..1284bcd4a7 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -29,12 +29,16 @@ class Scheduler { protected: struct SchedulerItem { + // Ordered by size to minimize padding Component *component; - std::string name; - enum Type { TIMEOUT, INTERVAL } type; uint32_t interval; + // 64-bit time to handle millis() rollover. The scheduler combines the 32-bit millis() + // with a 16-bit rollover counter to create a 64-bit time that won't roll over for + // billions of years. This ensures correct scheduling even when devices run for months. uint64_t next_execution_; + std::string name; std::function callback; + enum Type : uint8_t { TIMEOUT, INTERVAL } type; bool remove; static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); From 2a45467bf62fcde5933d55213a582d19627946b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 04:10:09 +0200 Subject: [PATCH 094/293] Pre-reserve looping components vector to reduce memory allocations (#9177) --- esphome/core/application.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 49c1e5fd61..f64070fa3d 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -257,6 +257,17 @@ void Application::teardown_components(uint32_t timeout_ms) { } void Application::calculate_looping_components_() { + // Count total components that need looping + size_t total_looping = 0; + for (auto *obj : this->components_) { + if (obj->has_overridden_loop()) { + total_looping++; + } + } + + // Pre-reserve vector to avoid reallocations + this->looping_components_.reserve(total_looping); + // First add all active components for (auto *obj : this->components_) { if (obj->has_overridden_loop() && From 78ec9856fbcf9a01e760f57ec5c2233e1cc8ef4d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:23:41 +1000 Subject: [PATCH 095/293] [lvgl] Add start_value to bar; make values templatable and updateable (#9056) --- esphome/components/lvgl/schemas.py | 8 ++- esphome/components/lvgl/widgets/lv_bar.py | 59 +++++++++++++++-------- tests/components/lvgl/lvgl-package.yaml | 3 ++ 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index fdc8750d1d..a0be65c928 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -454,9 +454,13 @@ def container_validator(schema, widget_type: WidgetType): """ def validator(value): - result = schema if w_sch := widget_type.schema: - result = result.extend(w_sch) + if isinstance(w_sch, dict): + w_sch = cv.Schema(w_sch) + # order is important here to preserve extras + result = w_sch.extend(schema) + else: + result = schema ltype = df.TYPE_NONE if value and (layout := value.get(df.CONF_LAYOUT)): if not isinstance(layout, dict): diff --git a/esphome/components/lvgl/widgets/lv_bar.py b/esphome/components/lvgl/widgets/lv_bar.py index 57209370c0..f0fdd6d278 100644 --- a/esphome/components/lvgl/widgets/lv_bar.py +++ b/esphome/components/lvgl/widgets/lv_bar.py @@ -1,8 +1,15 @@ import esphome.config_validation as cv from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE -from ..defines import BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, CONF_MAIN, literal -from ..lv_validation import animated, get_start_value, lv_float +from ..defines import ( + BAR_MODES, + CONF_ANIMATED, + CONF_INDICATOR, + CONF_MAIN, + CONF_START_VALUE, + literal, +) +from ..lv_validation import animated, lv_int from ..lvcode import lv from ..types import LvNumber, NumberType from . import Widget @@ -10,22 +17,30 @@ from . import Widget # Note this file cannot be called "bar.py" because that name is disallowed. CONF_BAR = "bar" -BAR_MODIFY_SCHEMA = cv.Schema( - { - cv.Optional(CONF_VALUE): lv_float, - cv.Optional(CONF_ANIMATED, default=True): animated, - } -) + + +def validate_bar(config): + if config.get(CONF_MODE) != "LV_BAR_MODE_RANGE" and CONF_START_VALUE in config: + raise cv.Invalid( + f"{CONF_START_VALUE} is only allowed when {CONF_MODE} is set to 'RANGE'" + ) + if (CONF_MIN_VALUE in config) != (CONF_MAX_VALUE in config): + raise cv.Invalid( + f"If either {CONF_MIN_VALUE} or {CONF_MAX_VALUE} is set, both must be set" + ) + return config + BAR_SCHEMA = cv.Schema( { - cv.Optional(CONF_VALUE): lv_float, - cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, - cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, - cv.Optional(CONF_MODE, default="NORMAL"): BAR_MODES.one_of, + cv.Optional(CONF_VALUE): lv_int, + cv.Optional(CONF_START_VALUE): lv_int, + cv.Optional(CONF_MIN_VALUE): lv_int, + cv.Optional(CONF_MAX_VALUE): lv_int, + cv.Optional(CONF_MODE): BAR_MODES.one_of, cv.Optional(CONF_ANIMATED, default=True): animated, } -) +).add_extra(validate_bar) class BarType(NumberType): @@ -35,17 +50,23 @@ class BarType(NumberType): LvNumber("lv_bar_t"), parts=(CONF_MAIN, CONF_INDICATOR), schema=BAR_SCHEMA, - modify_schema=BAR_MODIFY_SCHEMA, ) async def to_code(self, w: Widget, config): var = w.obj + if mode := config.get(CONF_MODE): + lv.bar_set_mode(var, literal(mode)) + is_animated = literal(config[CONF_ANIMATED]) if CONF_MIN_VALUE in config: - lv.bar_set_range(var, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) - lv.bar_set_mode(var, literal(config[CONF_MODE])) - value = await get_start_value(config) - if value is not None: - lv.bar_set_value(var, value, literal(config[CONF_ANIMATED])) + lv.bar_set_range( + var, + await lv_int.process(config[CONF_MIN_VALUE]), + await lv_int.process(config[CONF_MAX_VALUE]), + ) + if value := await lv_int.process(config.get(CONF_VALUE)): + lv.bar_set_value(var, value, is_animated) + if start_value := await lv_int.process(config.get(CONF_START_VALUE)): + lv.bar_set_start_value(var, start_value, is_animated) @property def animated(self): diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index d8452bdd2a..f32b09d0e6 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -728,12 +728,15 @@ lvgl: value: 30 max_value: 100 min_value: 10 + start_value: 20 mode: range on_click: then: - lvgl.bar.update: id: bar_id value: !lambda return (int)((float)rand() / RAND_MAX * 100); + start_value: !lambda return (int)((float)rand() / RAND_MAX * 100); + mode: symmetrical - logger.log: format: "bar value %f" args: [x] From 41c78521280e0376f5f2e0967cba784f44428504 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:25:26 +1000 Subject: [PATCH 096/293] [lvgl] Use styles instead of object properties for themes (#9116) --- esphome/components/lvgl/styles.py | 40 ++++++++++++--------- esphome/components/lvgl/widgets/__init__.py | 14 ++++++-- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index b59ff513e2..426dd3f229 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -3,7 +3,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.core import ID -from esphome.cpp_generator import MockObj from .defines import ( CONF_STYLE_DEFINITIONS, @@ -13,12 +12,13 @@ from .defines import ( literal, ) from .helpers import add_lv_use -from .lvcode import LambdaContext, LocalVariable, lv, lv_assign, lv_variable +from .lvcode import LambdaContext, LocalVariable, lv from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, STYLE_REMAP -from .types import ObjUpdateAction, lv_lambda_t, lv_obj_t, lv_obj_t_ptr, lv_style_t +from .types import ObjUpdateAction, lv_obj_t, lv_style_t from .widgets import ( Widget, add_widgets, + collect_parts, set_obj_properties, theme_widget_map, wait_for_widgets, @@ -37,12 +37,18 @@ async def style_set(svar, style): lv.call(f"style_set_{remapped_prop}", svar, literal(value)) +async def create_style(style, id_name): + style_id = ID(id_name, True, lv_style_t) + svar = cg.new_Pvariable(style_id) + lv.style_init(svar) + await style_set(svar, style) + return svar + + async def styles_to_code(config): """Convert styles to C__ code.""" for style in config.get(CONF_STYLE_DEFINITIONS, ()): - svar = cg.new_Pvariable(style[CONF_ID]) - lv.style_init(svar) - await style_set(svar, style) + await create_style(style, style[CONF_ID].id) @automation.register_action( @@ -68,16 +74,18 @@ async def theme_to_code(config): if theme := config.get(CONF_THEME): add_lv_use(CONF_THEME) for w_name, style in theme.items(): - if not isinstance(style, dict): - continue - - lname = "lv_theme_apply_" + w_name - apply = lv_variable(lv_lambda_t, lname) - theme_widget_map[w_name] = apply - ow = Widget.create("obj", MockObj(ID("obj")), obj_spec) - async with LambdaContext([(lv_obj_t_ptr, "obj")], where=w_name) as context: - await set_obj_properties(ow, style) - lv_assign(apply, await context.get_lambda()) + # Work around Python 3.10 bug with nested async comprehensions + # With Python 3.11 this could be simplified + styles = {} + for part, states in collect_parts(style).items(): + styles[part] = { + state: await create_style( + props, + "_lv_theme_style_" + w_name + "_" + part + "_" + state, + ) + for state, props in states.items() + } + theme_widget_map[w_name] = styles async def add_top_layer(lv_component, config): diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index 9d53c0df26..a8cb8dce33 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -6,7 +6,7 @@ from esphome.config_validation import Invalid from esphome.const import CONF_DEFAULT, CONF_GROUP, CONF_ID, CONF_STATE, CONF_TYPE from esphome.core import ID, TimePeriod from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import CallExpression, MockObj +from esphome.cpp_generator import MockObj from ..defines import ( CONF_FLEX_ALIGN_CROSS, @@ -453,7 +453,17 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent): w = Widget.create(wid, var, spec, w_cnfig) if theme := theme_widget_map.get(w_type): - lv_add(CallExpression(theme, w.obj)) + for part, states in theme.items(): + part = "LV_PART_" + part.upper() + for state, style in states.items(): + state = "LV_STATE_" + state.upper() + if state == "LV_STATE_DEFAULT": + lv_state = literal(part) + elif part == "LV_PART_MAIN": + lv_state = literal(state) + else: + lv_state = join_enums((state, part)) + lv.obj_add_style(w.obj, style, lv_state) await set_obj_properties(w, w_cnfig) await add_widgets(w, w_cnfig) await spec.to_code(w, w_cnfig) From 87df3596a2236ad3c503fc4b1c75bbbe843364d4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:41:06 +1200 Subject: [PATCH 097/293] [config validation] Add more ip address / network validators (#9181) --- esphome/config_validation.py | 45 +++++++++++++++++++++++++++++++++++- esphome/yaml_util.py | 3 ++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 964f533215..bf69b81bb5 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -3,7 +3,15 @@ from contextlib import contextmanager from dataclasses import dataclass from datetime import datetime -from ipaddress import AddressValueError, IPv4Address, ip_address +from ipaddress import ( + AddressValueError, + IPv4Address, + IPv4Network, + IPv6Address, + IPv6Network, + ip_address, + ip_network, +) import logging import os import re @@ -1176,6 +1184,14 @@ def ipv4address(value): return address +def ipv6address(value): + try: + address = IPv6Address(value) + except AddressValueError as exc: + raise Invalid(f"{value} is not a valid IPv6 address") from exc + return address + + def ipv4address_multi_broadcast(value): address = ipv4address(value) if not (address.is_multicast or (address == IPv4Address("255.255.255.255"))): @@ -1193,6 +1209,33 @@ def ipaddress(value): return address +def ipv4network(value): + """Validate that the value is a valid IPv4 network.""" + try: + network = IPv4Network(value, strict=False) + except ValueError as exc: + raise Invalid(f"{value} is not a valid IPv4 network") from exc + return network + + +def ipv6network(value): + """Validate that the value is a valid IPv6 network.""" + try: + network = IPv6Network(value, strict=False) + except ValueError as exc: + raise Invalid(f"{value} is not a valid IPv6 network") from exc + return network + + +def ipnetwork(value): + """Validate that the value is a valid IP network.""" + try: + network = ip_network(value, strict=False) + except ValueError as exc: + raise Invalid(f"{value} is not a valid IP network") from exc + return network + + def _valid_topic(value): """Validate that this is a valid topic name/filter.""" if value is None: # Used to disable publishing and subscribing diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 78deec8e65..bd1806affc 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -5,7 +5,7 @@ import fnmatch import functools import inspect from io import BytesIO, TextIOBase, TextIOWrapper -from ipaddress import _BaseAddress +from ipaddress import _BaseAddress, _BaseNetwork import logging import math import os @@ -621,6 +621,7 @@ ESPHomeDumper.add_multi_representer(str, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(int, ESPHomeDumper.represent_int) ESPHomeDumper.add_multi_representer(float, ESPHomeDumper.represent_float) ESPHomeDumper.add_multi_representer(_BaseAddress, ESPHomeDumper.represent_stringify) +ESPHomeDumper.add_multi_representer(_BaseNetwork, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(MACAddress, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(TimePeriod, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(Lambda, ESPHomeDumper.represent_lambda) From aecaffa2f5a9e35d1aa0773b17f07faa10dc8c98 Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Sun, 22 Jun 2025 23:41:29 -0400 Subject: [PATCH 098/293] Fixes for setup of OpenThread either using TLV or entering Credentials directly (#9157) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/openthread/__init__.py | 10 +++++----- esphome/components/openthread/openthread.cpp | 6 +++--- esphome/components/openthread/tlv.py | 7 +++++++ tests/components/openthread/test.esp32-c6-idf.yaml | 2 ++ 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 5b1ea491e3..393c47e720 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -46,7 +46,7 @@ def set_sdkconfig_options(config): add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", config[CONF_PAN_ID]) add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_CHANNEL", config[CONF_CHANNEL]) add_idf_sdkconfig_option( - "CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{config[CONF_NETWORK_KEY]:X}" + "CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{config[CONF_NETWORK_KEY]:X}".lower() ) if network_name := config.get(CONF_NETWORK_NAME): @@ -54,14 +54,14 @@ def set_sdkconfig_options(config): if (ext_pan_id := config.get(CONF_EXT_PAN_ID)) is not None: add_idf_sdkconfig_option( - "CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}" + "CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}".lower() ) if (mesh_local_prefix := config.get(CONF_MESH_LOCAL_PREFIX)) is not None: add_idf_sdkconfig_option( - "CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix:X}" + "CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix}".lower() ) if (pskc := config.get(CONF_PSKC)) is not None: - add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}") + add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}".lower()) if CONF_FORCE_DATASET in config: if config[CONF_FORCE_DATASET]: @@ -98,7 +98,7 @@ _CONNECTION_SCHEMA = cv.Schema( cv.Optional(CONF_EXT_PAN_ID): cv.hex_int, cv.Optional(CONF_NETWORK_NAME): cv.string_strict, cv.Optional(CONF_PSKC): cv.hex_int, - cv.Optional(CONF_MESH_LOCAL_PREFIX): cv.hex_int, + cv.Optional(CONF_MESH_LOCAL_PREFIX): cv.ipv6network, } ) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index f40a56952a..24b3c23960 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -137,7 +137,7 @@ void OpenThreadSrpComponent::setup() { // Copy the mdns services to our local instance so that the c_str pointers remain valid for the lifetime of this // component this->mdns_services_ = this->mdns_->get_services(); - ESP_LOGW(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size()); + ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size()); for (const auto &service : this->mdns_services_) { otSrpClientBuffersServiceEntry *entry = otSrpClientBuffersAllocateService(instance); if (!entry) { @@ -185,11 +185,11 @@ void OpenThreadSrpComponent::setup() { if (error != OT_ERROR_NONE) { ESP_LOGW(TAG, "Failed to add service: %s", otThreadErrorToString(error)); } - ESP_LOGW(TAG, "Added service: %s", full_service.c_str()); + ESP_LOGD(TAG, "Added service: %s", full_service.c_str()); } otSrpClientEnableAutoStartMode(instance, srp_start_callback, nullptr); - ESP_LOGW(TAG, "Finished SRP setup"); + ESP_LOGD(TAG, "Finished SRP setup"); } void *OpenThreadSrpComponent::pool_alloc_(size_t size) { diff --git a/esphome/components/openthread/tlv.py b/esphome/components/openthread/tlv.py index 45c8c47227..4a7d21c47d 100644 --- a/esphome/components/openthread/tlv.py +++ b/esphome/components/openthread/tlv.py @@ -1,5 +1,6 @@ # Sourced from https://gist.github.com/agners/0338576e0003318b63ec1ea75adc90f9 import binascii +import ipaddress from esphome.const import CONF_CHANNEL @@ -37,6 +38,12 @@ def parse_tlv(tlv) -> dict: if tag in TLV_TYPES: if tag == 3: output[TLV_TYPES[tag]] = val.decode("utf-8") + elif tag == 7: + mesh_local_prefix = binascii.hexlify(val).decode("utf-8") + mesh_local_prefix_str = f"{mesh_local_prefix}0000000000000000" + ipv6_bytes = bytes.fromhex(mesh_local_prefix_str) + ipv6_address = ipaddress.IPv6Address(ipv6_bytes) + output[TLV_TYPES[tag]] = f"{ipv6_address}/64" else: output[TLV_TYPES[tag]] = int.from_bytes(val) return output diff --git a/tests/components/openthread/test.esp32-c6-idf.yaml b/tests/components/openthread/test.esp32-c6-idf.yaml index 482fd1a453..f53b323bec 100644 --- a/tests/components/openthread/test.esp32-c6-idf.yaml +++ b/tests/components/openthread/test.esp32-c6-idf.yaml @@ -8,4 +8,6 @@ openthread: pan_id: 0x8f28 ext_pan_id: 0xd63e8e3e495ebbc3 pskc: 0xc23a76e98f1a6483639b1ac1271e2e27 + mesh_local_prefix: fd53:145f:ed22:ad81::/64 force_dataset: true + From cd22723623c78933764ff246fabd92eadeac87af Mon Sep 17 00:00:00 2001 From: myhomeiot <70070601+myhomeiot@users.noreply.github.com> Date: Mon, 23 Jun 2025 06:42:20 +0300 Subject: [PATCH 099/293] Restore access to BLEScanResult as get_scan_result (#9148) --- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp | 1 + esphome/components/esp32_ble_tracker/esp32_ble_tracker.h | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 4785c29230..d950ccb5f1 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -522,6 +522,7 @@ optional ESPBLEiBeacon::from_manufacturer_data(const ServiceData } void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) { + this->scan_result_ = &scan_result; for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++) this->address_[i] = scan_result.bda[i]; this->address_type_ = static_cast(scan_result.ble_addr_type); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 414c9f4b48..f5ed75a93e 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -85,6 +85,9 @@ class ESPBTDevice { const std::vector &get_service_datas() const { return service_datas_; } + // Exposed through a function for use in lambdas + const BLEScanResult &get_scan_result() const { return *scan_result_; } + bool resolve_irk(const uint8_t *irk) const; optional get_ibeacon() const { @@ -111,6 +114,7 @@ class ESPBTDevice { std::vector service_uuids_{}; std::vector manufacturer_datas_{}; std::vector service_datas_{}; + const BLEScanResult *scan_result_{nullptr}; }; class ESP32BLETracker; From 1a47164876cd59dc7e4f7aeafda5829916e07f6d Mon Sep 17 00:00:00 2001 From: JonasB2497 <45214989+JonasB2497@users.noreply.github.com> Date: Mon, 23 Jun 2025 06:47:47 +0200 Subject: [PATCH 100/293] Feature fontmetrics (#8978) --- esphome/components/font/__init__.py | 174 +++++++++++++++------------- esphome/components/font/font.cpp | 11 +- esphome/components/font/font.h | 21 +++- 3 files changed, 119 insertions(+), 87 deletions(-) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index be88fdb957..7d9a35647e 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -1,6 +1,7 @@ from collections.abc import MutableMapping import functools import hashlib +from itertools import accumulate import logging import os from pathlib import Path @@ -468,8 +469,9 @@ class EFont: class GlyphInfo: - def __init__(self, data_len, advance, offset_x, offset_y, width, height): - self.data_len = data_len + def __init__(self, glyph, data, advance, offset_x, offset_y, width, height): + self.glyph = glyph + self.bitmap_data = data self.advance = advance self.offset_x = offset_x self.offset_y = offset_y @@ -477,6 +479,62 @@ class GlyphInfo: self.height = height +def glyph_to_glyphinfo(glyph, font, size, bpp): + scale = 256 // (1 << bpp) + if not font.is_scalable: + sizes = [pt_to_px(x.size) for x in font.available_sizes] + if size in sizes: + font.select_size(sizes.index(size)) + else: + font.set_pixel_sizes(size, 0) + flags = FT_LOAD_RENDER + if bpp != 1: + flags |= FT_LOAD_NO_BITMAP + else: + flags |= FT_LOAD_TARGET_MONO + font.load_char(glyph, flags) + width = font.glyph.bitmap.width + height = font.glyph.bitmap.rows + buffer = font.glyph.bitmap.buffer + pitch = font.glyph.bitmap.pitch + glyph_data = [0] * ((height * width * bpp + 7) // 8) + src_mode = font.glyph.bitmap.pixel_mode + pos = 0 + for y in range(height): + for x in range(width): + if src_mode == ft_pixel_mode_mono: + pixel = ( + (1 << bpp) - 1 + if buffer[y * pitch + x // 8] & (1 << (7 - x % 8)) + else 0 + ) + else: + pixel = buffer[y * pitch + x] // scale + for bit_num in range(bpp): + if pixel & (1 << (bpp - bit_num - 1)): + glyph_data[pos // 8] |= 0x80 >> (pos % 8) + pos += 1 + ascender = pt_to_px(font.size.ascender) + if ascender == 0: + if not font.is_scalable: + ascender = size + else: + _LOGGER.error( + "Unable to determine ascender of font %s %s", + font.family_name, + font.style_name, + ) + return GlyphInfo( + glyph, + glyph_data, + pt_to_px(font.glyph.metrics.horiAdvance), + font.glyph.bitmap_left, + ascender - font.glyph.bitmap_top, + width, + height, + ) + + async def to_code(config): """ Collect all glyph codepoints, construct a map from a codepoint to a font file. @@ -506,98 +564,47 @@ async def to_code(config): codepoints = list(point_set) codepoints.sort(key=functools.cmp_to_key(glyph_comparator)) - glyph_args = {} - data = [] bpp = config[CONF_BPP] - scale = 256 // (1 << bpp) size = config[CONF_SIZE] # create the data array for all glyphs - for codepoint in codepoints: - font = point_font_map[codepoint] - if not font.is_scalable: - sizes = [pt_to_px(x.size) for x in font.available_sizes] - if size in sizes: - font.select_size(sizes.index(size)) - else: - font.set_pixel_sizes(size, 0) - flags = FT_LOAD_RENDER - if bpp != 1: - flags |= FT_LOAD_NO_BITMAP - else: - flags |= FT_LOAD_TARGET_MONO - font.load_char(codepoint, flags) - width = font.glyph.bitmap.width - height = font.glyph.bitmap.rows - buffer = font.glyph.bitmap.buffer - pitch = font.glyph.bitmap.pitch - glyph_data = [0] * ((height * width * bpp + 7) // 8) - src_mode = font.glyph.bitmap.pixel_mode - pos = 0 - for y in range(height): - for x in range(width): - if src_mode == ft_pixel_mode_mono: - pixel = ( - (1 << bpp) - 1 - if buffer[y * pitch + x // 8] & (1 << (7 - x % 8)) - else 0 - ) - else: - pixel = buffer[y * pitch + x] // scale - for bit_num in range(bpp): - if pixel & (1 << (bpp - bit_num - 1)): - glyph_data[pos // 8] |= 0x80 >> (pos % 8) - pos += 1 - ascender = pt_to_px(font.size.ascender) - if ascender == 0: - if not font.is_scalable: - ascender = size - else: - _LOGGER.error( - "Unable to determine ascender of font %s", config[CONF_FILE] - ) - glyph_args[codepoint] = GlyphInfo( - len(data), - pt_to_px(font.glyph.metrics.horiAdvance), - font.glyph.bitmap_left, - ascender - font.glyph.bitmap_top, - width, - height, - ) - data += glyph_data - - rhs = [HexInt(x) for x in data] + glyph_args = [ + glyph_to_glyphinfo(x, point_font_map[x], size, bpp) for x in codepoints + ] + rhs = [HexInt(x) for x in flatten([x.bitmap_data for x in glyph_args])] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) # Create the glyph table that points to data in the above array. - glyph_initializer = [] - for codepoint in codepoints: - glyph_initializer.append( - cg.StructInitializer( - GlyphData, - ( - "a_char", - cg.RawExpression( - f"(const uint8_t *){cpp_string_escape(codepoint)}" - ), - ), - ( - "data", - cg.RawExpression( - f"{str(prog_arr)} + {str(glyph_args[codepoint].data_len)}" - ), - ), - ("advance", glyph_args[codepoint].advance), - ("offset_x", glyph_args[codepoint].offset_x), - ("offset_y", glyph_args[codepoint].offset_y), - ("width", glyph_args[codepoint].width), - ("height", glyph_args[codepoint].height), - ) + glyph_initializer = [ + cg.StructInitializer( + GlyphData, + ( + "a_char", + cg.RawExpression(f"(const uint8_t *){cpp_string_escape(x.glyph)}"), + ), + ( + "data", + cg.RawExpression(f"{str(prog_arr)} + {str(y - len(x.bitmap_data))}"), + ), + ("advance", x.advance), + ("offset_x", x.offset_x), + ("offset_y", x.offset_y), + ("width", x.width), + ("height", x.height), ) + for (x, y) in zip( + glyph_args, list(accumulate([len(x.bitmap_data) for x in glyph_args])) + ) + ] glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer) font_height = pt_to_px(base_font.size.height) ascender = pt_to_px(base_font.size.ascender) + descender = abs(pt_to_px(base_font.size.descender)) + g = glyph_to_glyphinfo("x", base_font, size, bpp) + xheight = g.height if len(g.bitmap_data) > 1 else 0 + g = glyph_to_glyphinfo("X", base_font, size, bpp) + capheight = g.height if len(g.bitmap_data) > 1 else 0 if font_height == 0: if not base_font.is_scalable: font_height = size @@ -610,5 +617,8 @@ async def to_code(config): len(glyph_initializer), ascender, font_height, + descender, + xheight, + capheight, bpp, ) diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp index 32464d87ee..8b2420ac07 100644 --- a/esphome/components/font/font.cpp +++ b/esphome/components/font/font.cpp @@ -45,8 +45,15 @@ void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { *height = this->glyph_data_->height; } -Font::Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp) - : baseline_(baseline), height_(height), bpp_(bpp) { +Font::Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, + uint8_t bpp) + : baseline_(baseline), + height_(height), + descender_(descender), + linegap_(height - baseline - descender), + xheight_(xheight), + capheight_(capheight), + bpp_(bpp) { glyphs_.reserve(data_nr); for (int i = 0; i < data_nr; ++i) glyphs_.emplace_back(&data[i]); diff --git a/esphome/components/font/font.h b/esphome/components/font/font.h index 992c77cb9f..28832d647d 100644 --- a/esphome/components/font/font.h +++ b/esphome/components/font/font.h @@ -50,11 +50,17 @@ class Font public: /** Construct the font with the given glyphs. * - * @param glyphs A vector of glyphs, must be sorted lexicographically. + * @param data A vector of glyphs, must be sorted lexicographically. + * @param data_nr The number of glyphs in data. * @param baseline The y-offset from the top of the text to the baseline. - * @param bottom The y-offset from the top of the text to the bottom (i.e. height). + * @param height The y-offset from the top of the text to the bottom. + * @param descender The y-offset from the baseline to the lowest stroke in the font (e.g. from letters like g or p). + * @param xheight The height of lowercase letters, usually measured at the "x" glyph. + * @param capheight The height of capital letters, usually measured at the "X" glyph. + * @param bpp The bits per pixel used for this font. Used to read data out of the glyph bitmaps. */ - Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp = 1); + Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, + uint8_t bpp = 1); int match_next_glyph(const uint8_t *str, int *match_length); @@ -65,6 +71,11 @@ class Font #endif inline int get_baseline() { return this->baseline_; } inline int get_height() { return this->height_; } + inline int get_ascender() { return this->baseline_; } + inline int get_descender() { return this->descender_; } + inline int get_linegap() { return this->linegap_; } + inline int get_xheight() { return this->xheight_; } + inline int get_capheight() { return this->capheight_; } inline int get_bpp() { return this->bpp_; } const std::vector> &get_glyphs() const { return glyphs_; } @@ -73,6 +84,10 @@ class Font std::vector> glyphs_; int baseline_; int height_; + int descender_; + int linegap_; + int xheight_; + int capheight_; uint8_t bpp_; // bits per pixel }; From 2ad266582f61db675f0bd4b84b9b1927bfd211b8 Mon Sep 17 00:00:00 2001 From: Gustavo Ambrozio Date: Mon, 23 Jun 2025 00:40:07 -1000 Subject: [PATCH 101/293] [online_image] Allow suppressing update on url change (#8885) --- esphome/components/online_image/__init__.py | 5 +++++ esphome/components/online_image/online_image.h | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 9380cf1b1b..3f15db6e50 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -34,6 +34,7 @@ MULTI_CONF = True CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" CONF_PLACEHOLDER = "placeholder" +CONF_UPDATE = "update" _LOGGER = logging.getLogger(__name__) @@ -167,6 +168,7 @@ SET_URL_SCHEMA = cv.Schema( { cv.GenerateID(): cv.use_id(OnlineImage), cv.Required(CONF_URL): cv.templatable(cv.url), + cv.Optional(CONF_UPDATE, default=True): cv.templatable(bool), } ) @@ -188,6 +190,9 @@ async def online_image_action_to_code(config, action_id, template_arg, args): if CONF_URL in config: template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) cg.add(var.set_url(template_)) + if CONF_UPDATE in config: + template_ = await cg.templatable(config[CONF_UPDATE], args, bool) + cg.add(var.set_update(template_)) return var diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 6ed9c7956f..6a2144538f 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -201,9 +201,12 @@ template class OnlineImageSetUrlAction : public Action { public: OnlineImageSetUrlAction(OnlineImage *parent) : parent_(parent) {} TEMPLATABLE_VALUE(std::string, url) + TEMPLATABLE_VALUE(bool, update) void play(Ts... x) override { this->parent_->set_url(this->url_.value(x...)); - this->parent_->update(); + if (this->update_.value(x...)) { + this->parent_->update(); + } } protected: From ac942e0670e9afe9e8f8ca6903f3788320c674b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 19:58:32 +0200 Subject: [PATCH 102/293] Bump aioesphomeapi from 33.1.0 to 33.1.1 (#9187) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 01bbfa91c0..3f306fe4fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.9.0 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==33.1.0 +aioesphomeapi==33.1.1 zeroconf==0.147.0 puremagic==1.29 ruamel.yaml==0.18.14 # dashboard_import From a35e476be5b0ec14ae04695c5906c583977df59c Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 23 Jun 2025 13:31:20 -0600 Subject: [PATCH 103/293] [opt3001] New component (#6625) Co-authored-by: Keith Burzinski --- CODEOWNERS | 1 + esphome/components/opt3001/__init__.py | 0 esphome/components/opt3001/opt3001.cpp | 122 ++++++++++++++++++ esphome/components/opt3001/opt3001.h | 27 ++++ esphome/components/opt3001/sensor.py | 35 +++++ tests/components/opt3001/common.yaml | 10 ++ tests/components/opt3001/test.esp32-ard.yaml | 5 + .../components/opt3001/test.esp32-c3-ard.yaml | 5 + .../components/opt3001/test.esp32-c3-idf.yaml | 5 + tests/components/opt3001/test.esp32-idf.yaml | 5 + .../components/opt3001/test.esp8266-ard.yaml | 5 + tests/components/opt3001/test.rp2040-ard.yaml | 5 + 12 files changed, 225 insertions(+) create mode 100644 esphome/components/opt3001/__init__.py create mode 100644 esphome/components/opt3001/opt3001.cpp create mode 100644 esphome/components/opt3001/opt3001.h create mode 100644 esphome/components/opt3001/sensor.py create mode 100644 tests/components/opt3001/common.yaml create mode 100644 tests/components/opt3001/test.esp32-ard.yaml create mode 100644 tests/components/opt3001/test.esp32-c3-ard.yaml create mode 100644 tests/components/opt3001/test.esp32-c3-idf.yaml create mode 100644 tests/components/opt3001/test.esp32-idf.yaml create mode 100644 tests/components/opt3001/test.esp8266-ard.yaml create mode 100644 tests/components/opt3001/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index ebbc8732ea..83d64a8850 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -323,6 +323,7 @@ esphome/components/one_wire/* @ssieb esphome/components/online_image/* @clydebarrow @guillempages esphome/components/opentherm/* @olegtarasov esphome/components/openthread/* @mrene +esphome/components/opt3001/* @ccutrer esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/packet_transport/* @clydebarrow diff --git a/esphome/components/opt3001/__init__.py b/esphome/components/opt3001/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/opt3001/opt3001.cpp b/esphome/components/opt3001/opt3001.cpp new file mode 100644 index 0000000000..2d65f1090d --- /dev/null +++ b/esphome/components/opt3001/opt3001.cpp @@ -0,0 +1,122 @@ +#include "opt3001.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace opt3001 { + +static const char *const TAG = "opt3001.sensor"; + +static const uint8_t OPT3001_REG_RESULT = 0x00; +static const uint8_t OPT3001_REG_CONFIGURATION = 0x01; +// See datasheet for full description of each bit. +static const uint16_t OPT3001_CONFIGURATION_RANGE_FULL = 0b1100000000000000; +static const uint16_t OPT3001_CONFIGURATION_CONVERSION_TIME_800 = 0b100000000000; +static const uint16_t OPT3001_CONFIGURATION_CONVERSION_MODE_MASK = 0b11000000000; +static const uint16_t OPT3001_CONFIGURATION_CONVERSION_MODE_SINGLE_SHOT = 0b01000000000; +static const uint16_t OPT3001_CONFIGURATION_CONVERSION_MODE_SHUTDOWN = 0b00000000000; +// tl;dr: Configure an automatic-ranged, 800ms single shot reading, +// with INT processing disabled +static const uint16_t OPT3001_CONFIGURATION_FULL_RANGE_ONE_SHOT = OPT3001_CONFIGURATION_RANGE_FULL | + OPT3001_CONFIGURATION_CONVERSION_TIME_800 | + OPT3001_CONFIGURATION_CONVERSION_MODE_SINGLE_SHOT; +static const uint16_t OPT3001_CONVERSION_TIME_800 = 825; // give it 25 extra ms; it seems to not be ready quite often + +/* +opt3001 properties: + +- e (exponent) = high 4 bits of result register +- m (mantissa) = low 12 bits of result register +- formula: (0.01 * 2^e) * m lx + +*/ + +void OPT3001Sensor::read_result_(const std::function &f) { + // ensure the single shot flag is clear, indicating it's done + uint16_t raw_value; + if (this->read(reinterpret_cast(&raw_value), 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Reading configuration register failed"); + f(NAN); + return; + } + raw_value = i2c::i2ctohs(raw_value); + + if ((raw_value & OPT3001_CONFIGURATION_CONVERSION_MODE_MASK) != OPT3001_CONFIGURATION_CONVERSION_MODE_SHUTDOWN) { + // not ready; wait 10ms and try again + ESP_LOGW(TAG, "Data not ready; waiting 10ms"); + this->set_timeout("opt3001_wait", 10, [this, f]() { read_result_(f); }); + return; + } + + if (this->read_register(OPT3001_REG_RESULT, reinterpret_cast(&raw_value), 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Reading result register failed"); + f(NAN); + return; + } + raw_value = i2c::i2ctohs(raw_value); + + uint8_t exponent = raw_value >> 12; + uint16_t mantissa = raw_value & 0b111111111111; + + double lx = 0.01 * pow(2.0, double(exponent)) * double(mantissa); + f(float(lx)); +} + +void OPT3001Sensor::read_lx_(const std::function &f) { + // turn on (after one-shot sensor automatically powers down) + uint16_t start_measurement = i2c::htoi2cs(OPT3001_CONFIGURATION_FULL_RANGE_ONE_SHOT); + if (this->write_register(OPT3001_REG_CONFIGURATION, reinterpret_cast(&start_measurement), 2) != + i2c::ERROR_OK) { + ESP_LOGW(TAG, "Triggering one shot measurement failed"); + f(NAN); + return; + } + + this->set_timeout("read", OPT3001_CONVERSION_TIME_800, [this, f]() { + if (this->write(&OPT3001_REG_CONFIGURATION, 1, true) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Starting configuration register read failed"); + f(NAN); + return; + } + + this->read_result_(f); + }); +} + +void OPT3001Sensor::dump_config() { + LOG_SENSOR("", "OPT3001", this); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } + + LOG_UPDATE_INTERVAL(this); +} + +void OPT3001Sensor::update() { + // Set a flag and skip just in case the sensor isn't responding, + // and we just keep waiting for it in read_result_. + // This way we don't end up with potentially boundless "threads" + // using up memory and eventually crashing the device + if (this->updating_) { + return; + } + this->updating_ = true; + + this->read_lx_([this](float val) { + this->updating_ = false; + + if (std::isnan(val)) { + this->status_set_warning(); + this->publish_state(NAN); + return; + } + ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val); + this->status_clear_warning(); + this->publish_state(val); + }); +} + +float OPT3001Sensor::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace opt3001 +} // namespace esphome diff --git a/esphome/components/opt3001/opt3001.h b/esphome/components/opt3001/opt3001.h new file mode 100644 index 0000000000..ae3fde5c54 --- /dev/null +++ b/esphome/components/opt3001/opt3001.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace opt3001 { + +/// This class implements support for the i2c-based OPT3001 ambient light sensor. +class OPT3001Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void dump_config() override; + void update() override; + float get_setup_priority() const override; + + protected: + // checks if one-shot is complete before reading the result and returning it + void read_result_(const std::function &f); + // begins a one-shot measurement + void read_lx_(const std::function &f); + + bool updating_{false}; +}; + +} // namespace opt3001 +} // namespace esphome diff --git a/esphome/components/opt3001/sensor.py b/esphome/components/opt3001/sensor.py new file mode 100644 index 0000000000..a5bbf0e8dd --- /dev/null +++ b/esphome/components/opt3001/sensor.py @@ -0,0 +1,35 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, + UNIT_LUX, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@ccutrer"] + +opt3001_ns = cg.esphome_ns.namespace("opt3001") + +OPT3001Sensor = opt3001_ns.class_( + "OPT3001Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + OPT3001Sensor, + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x44)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/tests/components/opt3001/common.yaml b/tests/components/opt3001/common.yaml new file mode 100644 index 0000000000..dab4f824f8 --- /dev/null +++ b/tests/components/opt3001/common.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_opt3001 + scl: ${scl_pin} + sda: ${sda_pin} + +sensor: + - platform: opt3001 + name: Living Room Brightness + address: 0x44 + update_interval: 30s diff --git a/tests/components/opt3001/test.esp32-ard.yaml b/tests/components/opt3001/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/opt3001/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/opt3001/test.esp32-c3-ard.yaml b/tests/components/opt3001/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/opt3001/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/opt3001/test.esp32-c3-idf.yaml b/tests/components/opt3001/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/opt3001/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/opt3001/test.esp32-idf.yaml b/tests/components/opt3001/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/opt3001/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/opt3001/test.esp8266-ard.yaml b/tests/components/opt3001/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/opt3001/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/opt3001/test.rp2040-ard.yaml b/tests/components/opt3001/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/opt3001/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml From 612c8d5841657e03d310ef37e0e4602f89221998 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 24 Jun 2025 09:43:40 +1000 Subject: [PATCH 104/293] [lvgl] Fix dangling pointer issue with qrcode (#9190) --- esphome/components/lvgl/widgets/qrcode.py | 10 ++++------ tests/components/lvgl/lvgl-package.yaml | 4 +++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py index 742b538938..7d8d13d8c4 100644 --- a/esphome/components/lvgl/widgets/qrcode.py +++ b/esphome/components/lvgl/widgets/qrcode.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome.const import CONF_SIZE, CONF_TEXT from esphome.cpp_generator import MockObjClass -from ..defines import CONF_MAIN, literal +from ..defines import CONF_MAIN from ..lv_validation import color, color_retmapper, lv_text from ..lvcode import LocalVariable, lv, lv_expr from ..schemas import TEXT_SCHEMA @@ -34,7 +34,7 @@ class QrCodeType(WidgetType): ) def get_uses(self): - return ("canvas", "img") + return ("canvas", "img", "label") def obj_creator(self, parent: MockObjClass, config: dict): dark_color = color_retmapper(config[CONF_DARK_COLOR]) @@ -45,10 +45,8 @@ class QrCodeType(WidgetType): async def to_code(self, w: Widget, config): if (value := config.get(CONF_TEXT)) is not None: value = await lv_text.process(value) - with LocalVariable( - "qr_text", cg.const_char_ptr, value, modifier="" - ) as str_obj: - lv.qrcode_update(w.obj, str_obj, literal(f"strlen({str_obj})")) + with LocalVariable("qr_text", cg.std_string, value, modifier="") as str_obj: + lv.qrcode_update(w.obj, str_obj.c_str(), str_obj.size()) qr_code_spec = QrCodeType() diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index f32b09d0e6..212e30c1eb 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -646,7 +646,9 @@ lvgl: on_click: lvgl.qrcode.update: id: lv_qr - text: homeassistant.io + text: + format: "A string with a number %d" + args: ['(int)(random_uint32() % 1000)'] - slider: min_value: 0 From 7ad6dab3839686ec64ba15de4d0a10c41e506346 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:31:38 +1200 Subject: [PATCH 105/293] [mqtt] Don't wait for connection unless configured to (#8933) --- esphome/components/mqtt/__init__.py | 4 ++++ esphome/components/mqtt/mqtt_client.cpp | 3 ++- esphome/components/mqtt/mqtt_client.h | 9 ++++++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 63d8da5788..f0d5a95d43 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -68,6 +68,7 @@ def AUTO_LOAD(): CONF_DISCOVER_IP = "discover_ip" CONF_IDF_SEND_ASYNC = "idf_send_async" +CONF_WAIT_FOR_CONNECTION = "wait_for_connection" def validate_message_just_topic(value): @@ -298,6 +299,7 @@ CONFIG_SCHEMA = cv.All( } ), cv.Optional(CONF_PUBLISH_NAN_AS_NONE, default=False): cv.boolean, + cv.Optional(CONF_WAIT_FOR_CONNECTION, default=False): cv.boolean, } ), validate_config, @@ -453,6 +455,8 @@ async def to_code(config): cg.add(var.set_publish_nan_as_none(config[CONF_PUBLISH_NAN_AS_NONE])) + cg.add(var.set_wait_for_connection(config[CONF_WAIT_FOR_CONNECTION])) + MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema( { diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index ceb56bdfbe..3ba1ac6077 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -176,7 +176,8 @@ void MQTTClientComponent::dump_config() { } } bool MQTTClientComponent::can_proceed() { - return network::is_disabled() || this->state_ == MQTT_CLIENT_DISABLED || this->is_connected(); + return network::is_disabled() || this->state_ == MQTT_CLIENT_DISABLED || this->is_connected() || + !this->wait_for_connection_; } void MQTTClientComponent::start_dnslookup_() { diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index c68b3c62eb..a95b122383 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -4,11 +4,11 @@ #ifdef USE_MQTT -#include "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "esphome/core/log.h" #include "esphome/components/json/json_util.h" #include "esphome/components/network/ip_address.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/log.h" #if defined(USE_ESP32) #include "mqtt_backend_esp32.h" #elif defined(USE_ESP8266) @@ -267,6 +267,8 @@ class MQTTClientComponent : public Component { void set_publish_nan_as_none(bool publish_nan_as_none); bool is_publish_nan_as_none() const; + void set_wait_for_connection(bool wait_for_connection) { this->wait_for_connection_ = wait_for_connection; } + protected: void send_device_info_(); @@ -334,6 +336,7 @@ class MQTTClientComponent : public Component { optional disconnect_reason_{}; bool publish_nan_as_none_{false}; + bool wait_for_connection_{false}; }; extern MQTTClientComponent *global_mqtt_client; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) From 2df0ebd895220f8f8fd0b37bb929749f47f1a6ab Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Wed, 25 Jun 2025 01:31:23 +0200 Subject: [PATCH 106/293] [modbus_controller] Fix modbus read_lambda precision for non-floats or large integers (#9159) --- esphome/components/modbus/modbus.h | 8 +++ .../components/modbus_controller/__init__.py | 43 ++++++++++----- .../modbus_controller/modbus_controller.cpp | 21 ++++---- .../modbus_controller/modbus_controller.h | 53 ++++++++++++++++--- 4 files changed, 96 insertions(+), 29 deletions(-) diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index 4a78ed4aab..aebdbccc78 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -64,6 +64,14 @@ class ModbusDevice { this->parent_->send(this->address_, function, start_address, number_of_entities, payload_len, payload); } void send_raw(const std::vector &payload) { this->parent_->send_raw(payload); } + void send_error(uint8_t function_code, uint8_t exception_code) { + std::vector error_response; + error_response.reserve(3); + error_response.push_back(this->address_); + error_response.push_back(function_code | 0x80); + error_response.push_back(exception_code); + this->send_raw(error_response); + } // If more than one device is connected block sending a new command before a response is received bool waiting_for_response() { return parent_->waiting_for_response != 0; } diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 61b60498d0..8079b824b0 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -112,6 +112,22 @@ TYPE_REGISTER_MAP = { "FP32_R": 2, } +CPP_TYPE_REGISTER_MAP = { + "RAW": cg.uint16, + "U_WORD": cg.uint16, + "S_WORD": cg.int16, + "U_DWORD": cg.uint32, + "U_DWORD_R": cg.uint32, + "S_DWORD": cg.int32, + "S_DWORD_R": cg.int32, + "U_QWORD": cg.uint64, + "U_QWORD_R": cg.uint64, + "S_QWORD": cg.int64, + "S_QWORD_R": cg.int64, + "FP32": cg.float_, + "FP32_R": cg.float_, +} + ModbusCommandSentTrigger = modbus_controller_ns.class_( "ModbusCommandSentTrigger", automation.Trigger.template(cg.int_, cg.int_) ) @@ -285,21 +301,24 @@ async def to_code(config): cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES])) if CONF_SERVER_REGISTERS in config: for server_register in config[CONF_SERVER_REGISTERS]: + server_register_var = cg.new_Pvariable( + server_register[CONF_ID], + server_register[CONF_ADDRESS], + server_register[CONF_VALUE_TYPE], + TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]], + ) + cpp_type = CPP_TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]] cg.add( - var.add_server_register( - cg.new_Pvariable( - server_register[CONF_ID], - server_register[CONF_ADDRESS], - server_register[CONF_VALUE_TYPE], - TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]], - await cg.process_lambda( - server_register[CONF_READ_LAMBDA], - [], - return_type=cg.float_, - ), - ) + server_register_var.set_read_lambda( + cg.TemplateArguments(cpp_type), + await cg.process_lambda( + server_register[CONF_READ_LAMBDA], + [(cg.uint16, "address")], + return_type=cpp_type, + ), ) ) + cg.add(var.add_server_register(server_register_var)) await register_modbus_device(var, config) for conf in config.get(CONF_ON_COMMAND_SENT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 48ff868087..81e9ccf0a6 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -117,12 +117,17 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t bool found = false; for (auto *server_register : this->server_registers_) { if (server_register->address == current_address) { - float value = server_register->read_lambda(); + if (!server_register->read_lambda) { + break; + } + int64_t value = server_register->read_lambda(); + ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.", + server_register->address, static_cast(server_register->value_type), + server_register->register_count, server_register->format_value(value).c_str()); - ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %0.1f.", - server_register->address, static_cast(server_register->value_type), - server_register->register_count, value); - std::vector payload = float_to_payload(value, server_register->value_type); + std::vector payload; + payload.reserve(server_register->register_count * 2); + number_to_payload(payload, value, server_register->value_type); sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend()); current_address += server_register->register_count; found = true; @@ -132,11 +137,7 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t if (!found) { ESP_LOGW(TAG, "Could not match any register to address %02X. Sending exception response.", current_address); - std::vector error_response; - error_response.push_back(this->address_); - error_response.push_back(0x81); - error_response.push_back(0x02); - this->send_raw(error_response); + send_error(function_code, 0x02); return; } } diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index dfd52e44bc..11d27c4025 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -63,6 +63,10 @@ enum class SensorValueType : uint8_t { FP32_R = 0xD }; +inline bool value_type_is_float(SensorValueType v) { + return v == SensorValueType::FP32 || v == SensorValueType::FP32_R; +} + inline ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type) { switch (reg_type) { case ModbusRegisterType::COIL: @@ -253,18 +257,53 @@ class SensorItem { }; class ServerRegister { + using ReadLambda = std::function; + public: - ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count, - std::function read_lambda) { + ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count) { this->address = address; this->value_type = value_type; this->register_count = register_count; - this->read_lambda = std::move(read_lambda); } + + template void set_read_lambda(const std::function &&user_read_lambda) { + this->read_lambda = [this, user_read_lambda]() -> int64_t { + T user_value = user_read_lambda(this->address); + if constexpr (std::is_same_v) { + return bit_cast(user_value); + } else { + return static_cast(user_value); + } + }; + } + + // Formats a raw value into a string representation based on the value type for debugging + std::string format_value(int64_t value) const { + switch (this->value_type) { + case SensorValueType::U_WORD: + case SensorValueType::U_DWORD: + case SensorValueType::U_DWORD_R: + case SensorValueType::U_QWORD: + case SensorValueType::U_QWORD_R: + return std::to_string(static_cast(value)); + case SensorValueType::S_WORD: + case SensorValueType::S_DWORD: + case SensorValueType::S_DWORD_R: + case SensorValueType::S_QWORD: + case SensorValueType::S_QWORD_R: + return std::to_string(value); + case SensorValueType::FP32_R: + case SensorValueType::FP32: + return str_sprintf("%.1f", bit_cast(static_cast(value))); + default: + return std::to_string(value); + } + } + uint16_t address{0}; SensorValueType value_type{SensorValueType::RAW}; uint8_t register_count{0}; - std::function read_lambda; + ReadLambda read_lambda; }; // ModbusController::create_register_ranges_ tries to optimize register range @@ -444,7 +483,7 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { void on_modbus_data(const std::vector &data) override; /// called when a modbus error response was received void on_modbus_error(uint8_t function_code, uint8_t exception_code) override; - /// called when a modbus request (function code 3 or 4) was parsed without errors + /// called when a modbus request (function code 0x03 or 0x04) was parsed without errors void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final; /// default delegate called by process_modbus_data when a response has retrieved from the incoming queue void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector &data); @@ -529,7 +568,7 @@ inline float payload_to_float(const std::vector &data, const SensorItem int64_t number = payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask); float float_value; - if (item.sensor_value_type == SensorValueType::FP32 || item.sensor_value_type == SensorValueType::FP32_R) { + if (value_type_is_float(item.sensor_value_type)) { float_value = bit_cast(static_cast(number)); } else { float_value = static_cast(number); @@ -541,7 +580,7 @@ inline float payload_to_float(const std::vector &data, const SensorItem inline std::vector float_to_payload(float value, SensorValueType value_type) { int64_t val; - if (value_type == SensorValueType::FP32 || value_type == SensorValueType::FP32_R) { + if (value_type_is_float(value_type)) { val = bit_cast(value); } else { val = llroundf(value); From 9f831e91b326fee96fa187cee85e4b777f7d3031 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 24 Jun 2025 19:36:33 -0500 Subject: [PATCH 107/293] [helpers] Add ``format_mac_address_pretty`` function, migrate components (#9193) --- esphome/components/esp32_ble/ble.cpp | 10 ++++----- esphome/components/ld2410/ld2410.cpp | 21 +++---------------- esphome/components/ld2450/ld2450.cpp | 10 +++------ esphome/components/wifi/wifi_component.cpp | 5 ----- esphome/components/wifi/wifi_component.h | 2 -- .../wifi/wifi_component_esp32_arduino.cpp | 10 ++++----- .../wifi/wifi_component_esp8266.cpp | 13 ++++++------ .../wifi/wifi_component_esp_idf.cpp | 10 ++++----- .../wifi/wifi_component_libretiny.cpp | 10 ++++----- esphome/components/xiaomi_ble/xiaomi_ble.cpp | 2 +- esphome/core/helpers.cpp | 6 +++++- esphome/core/helpers.h | 2 ++ 12 files changed, 41 insertions(+), 60 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index cf63ad34d7..b10d1fe10a 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -4,6 +4,7 @@ #include "ble_event_pool.h" #include "esphome/core/application.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include @@ -516,13 +517,12 @@ void ESP32BLE::dump_config() { break; } ESP_LOGCONFIG(TAG, - "ESP32 BLE:\n" - " MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n" + "BLE:\n" + " MAC address: %s\n" " IO Capability: %s", - mac_address[0], mac_address[1], mac_address[2], mac_address[3], mac_address[4], mac_address[5], - io_capability_s); + format_mac_address_pretty(mac_address).c_str(), io_capability_s); } else { - ESP_LOGCONFIG(TAG, "ESP32 BLE: bluetooth stack is not enabled"); + ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled"); } } diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index d7007ae0bd..4272159da6 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -299,21 +299,6 @@ const char MAC_FMT[] = "%02X:%02X:%02X:%02X:%02X:%02X"; const std::string UNKNOWN_MAC("unknown"); const std::string NO_MAC("08:05:04:03:02:01"); -std::string format_mac(uint8_t *buffer) { - std::string::size_type mac_size = 256; - std::string mac; - do { - mac.resize(mac_size + 1); - mac_size = std::snprintf(&mac[0], mac.size(), MAC_FMT, buffer[10], buffer[11], buffer[12], buffer[13], buffer[14], - buffer[15]); - } while (mac_size + 1 > mac.size()); - mac.resize(mac_size); - if (mac == NO_MAC) { - return UNKNOWN_MAC; - } - return mac; -} - #ifdef USE_NUMBER std::function set_number_value(number::Number *n, float value) { float normalized_value = value * 1.0; @@ -406,11 +391,11 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { if (len < 20) { return false; } - this->mac_ = format_mac(buffer); - ESP_LOGV(TAG, "MAC Address is: %s", const_cast(this->mac_.c_str())); + this->mac_ = format_mac_address_pretty(&buffer[10]); + ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str()); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { - this->mac_text_sensor_->publish_state(this->mac_); + this->mac_text_sensor_->publish_state(this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_); } #endif #ifdef USE_SWITCH diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 519e4d89a3..4070c75fad 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -7,6 +7,7 @@ #include "esphome/components/sensor/sensor.h" #endif #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #define highbyte(val) (uint8_t)((val) >> 8) #define lowbyte(val) (uint8_t)((val) &0xff) @@ -96,11 +97,6 @@ static inline std::string get_direction(int16_t speed) { return STATIONARY; } -static inline std::string format_mac(uint8_t *buffer) { - return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, buffer[10], buffer[11], buffer[12], buffer[13], buffer[14], - buffer[15]); -} - static inline std::string format_version(uint8_t *buffer) { return str_sprintf("%u.%02X.%02X%02X%02X%02X", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]); @@ -613,7 +609,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { if (len < 20) { return false; } - this->mac_ = ld2450::format_mac(buffer); + this->mac_ = format_mac_address_pretty(&buffer[10]); ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str()); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { @@ -622,7 +618,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { #endif #ifdef USE_SWITCH if (this->bluetooth_switch_ != nullptr) { - this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC); + this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); } #endif break; diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 51ae1c9f8e..d717b68340 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -741,11 +741,6 @@ void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->po void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; } -std::string WiFiComponent::format_mac_addr(const uint8_t *mac) { - char buf[20]; - sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - return buf; -} bool WiFiComponent::is_captive_portal_active_() { #ifdef USE_CAPTIVE_PORTAL return captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 982007e47f..efd43077d1 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -321,8 +321,6 @@ class WiFiComponent : public Component { int32_t get_wifi_channel(); protected: - static std::string format_mac_addr(const uint8_t mac[6]); - #ifdef USE_WIFI_AP void setup_ap_config_(); #endif // USE_WIFI_AP diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index 3fc2c009db..a7877eb90b 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -550,7 +550,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, - format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); + format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); #if USE_NETWORK_IPV6 this->set_timeout(100, [] { WiFi.enableIPv6(); }); #endif /* USE_NETWORK_IPV6 */ @@ -566,7 +566,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); } else { ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); } uint8_t reason = it.reason; @@ -636,13 +636,13 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: { auto it = info.wifi_sta_connected; auto &mac = it.bssid; - ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_addr(mac).c_str()); + ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(mac).c_str()); break; } case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: { auto it = info.wifi_sta_disconnected; auto &mac = it.bssid; - ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_addr(mac).c_str()); + ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(mac).c_str()); break; } case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: { @@ -651,7 +651,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: { auto it = info.wifi_ap_probereqrecved; - ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi); break; } default: diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 594bc79e54..ae1daed8b5 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -496,7 +496,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { char buf[33]; memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; - ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_addr(it.bssid).c_str(), it.channel); + ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_address_pretty(it.bssid).c_str(), + it.channel); s_sta_connected = true; break; } @@ -510,7 +511,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { s_sta_connect_not_found = true; } else { ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_addr(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason))); + format_mac_address_pretty(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason))); s_sta_connect_error = true; } s_sta_connected = false; @@ -545,17 +546,17 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } case EVENT_SOFTAPMODE_STACONNECTED: { auto it = event->event_info.sta_connected; - ESP_LOGV(TAG, "AP client connected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); + ESP_LOGV(TAG, "AP client connected MAC=%s aid=%u", format_mac_address_pretty(it.mac).c_str(), it.aid); break; } case EVENT_SOFTAPMODE_STADISCONNECTED: { auto it = event->event_info.sta_disconnected; - ESP_LOGV(TAG, "AP client disconnected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); + ESP_LOGV(TAG, "AP client disconnected MAC=%s aid=%u", format_mac_address_pretty(it.mac).c_str(), it.aid); break; } case EVENT_SOFTAPMODE_PROBEREQRECVED: { auto it = event->event_info.ap_probereqrecved; - ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi); break; } #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) @@ -567,7 +568,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } case EVENT_SOFTAPMODE_DISTRIBUTE_STA_IP: { auto it = event->event_info.distribute_sta_ip; - ESP_LOGV(TAG, "AP Distribute Station IP MAC=%s IP=%s aid=%u", format_mac_addr(it.mac).c_str(), + ESP_LOGV(TAG, "AP Distribute Station IP MAC=%s IP=%s aid=%u", format_mac_address_pretty(it.mac).c_str(), format_ip_addr(it.ip).c_str(), it.aid); break; } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index e767e7ffc1..f0655a6d1d 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -691,7 +691,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, - format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); + format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); s_sta_connected = true; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) { @@ -708,7 +708,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { return; } else { ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); s_sta_connect_error = true; } s_sta_connected = false; @@ -780,15 +780,15 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_PROBEREQRECVED) { const auto &it = data->data.ap_probe_req_rx; - ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi); } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STACONNECTED) { const auto &it = data->data.ap_staconnected; - ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_addr(it.mac).c_str()); + ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(it.mac).c_str()); } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STADISCONNECTED) { const auto &it = data->data.ap_stadisconnected; - ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_addr(it.mac).c_str()); + ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(it.mac).c_str()); } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) { const auto &it = data->data.ip_ap_staipassigned; diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 0f7b688290..b15f710150 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -281,7 +281,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, - format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); + format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); break; } @@ -294,7 +294,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); } else { ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); } uint8_t reason = it.reason; @@ -349,13 +349,13 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: { auto it = info.wifi_sta_connected; auto &mac = it.bssid; - ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_addr(mac).c_str()); + ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(mac).c_str()); break; } case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: { auto it = info.wifi_sta_disconnected; auto &mac = it.bssid; - ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_addr(mac).c_str()); + ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(mac).c_str()); break; } case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: { @@ -364,7 +364,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: { auto it = info.wifi_ap_probereqrecved; - ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi); break; } default: diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index a80daa0b80..564870d74e 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -320,7 +320,7 @@ bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, c memcpy(mac_address + 4, mac_reverse + 1, 1); memcpy(mac_address + 5, mac_reverse, 1); ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption failed."); - ESP_LOGVV(TAG, " MAC address : %s", format_hex_pretty(mac_address, 6).c_str()); + ESP_LOGVV(TAG, " MAC address : %s", format_mac_address_pretty(mac_address).c_str()); ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str()); ESP_LOGVV(TAG, " Key : %s", format_hex_pretty(vector.key, vector.keysize).c_str()); ESP_LOGVV(TAG, " Iv : %s", format_hex_pretty(vector.iv, vector.ivsize).c_str()); diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index ec79cb8bbb..79dbb314c8 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -356,6 +356,10 @@ size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) { return chars; } +std::string format_mac_address_pretty(const uint8_t *mac) { + return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); +} + static char format_hex_char(uint8_t v) { return v >= 10 ? 'a' + (v - 10) : '0' + v; } std::string format_hex(const uint8_t *data, size_t length) { std::string ret; @@ -732,7 +736,7 @@ std::string get_mac_address() { std::string get_mac_address_pretty() { uint8_t mac[6]; get_mac_address_raw(mac); - return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return format_mac_address_pretty(mac); } #ifdef USE_ESP32 diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 477f260bf0..8bd5b813c7 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -402,6 +402,8 @@ template::value, int> = 0> optional< return parse_hex(str.c_str(), str.length()); } +/// Format the six-byte array \p mac into a MAC address. +std::string format_mac_address_pretty(const uint8_t mac[6]); /// Format the byte array \p data of length \p len in lowercased hex. std::string format_hex(const uint8_t *data, size_t length); /// Format the vector \p data in lowercased hex. From cf5197b68acb0dcc379fe2932cf44b3c179553ba Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 25 Jun 2025 03:15:50 -0500 Subject: [PATCH 108/293] [ld2450] Use ``App.get_loop_component_start_time()``, shorten log messages (#9192) --- esphome/components/ld2450/ld2450.cpp | 55 ++++++++++++++-------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 4070c75fad..718c853d22 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -6,6 +6,7 @@ #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" #endif +#include "esphome/core/application.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -116,7 +117,7 @@ void LD2450Component::setup() { } void LD2450Component::dump_config() { - ESP_LOGCONFIG(TAG, "HLK-LD2450 Human motion tracking radar module:"); + ESP_LOGCONFIG(TAG, "LD2450:"); #ifdef USE_BINARY_SENSOR LOG_BINARY_SENSOR(" ", "TargetBinarySensor", this->target_binary_sensor_); LOG_BINARY_SENSOR(" ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_); @@ -185,9 +186,9 @@ void LD2450Component::dump_config() { LOG_NUMBER(" ", "PresenceTimeoutNumber", this->presence_timeout_number_); #endif ESP_LOGCONFIG(TAG, - " Throttle : %ums\n" - " MAC Address : %s\n" - " Firmware version : %s", + " Throttle: %ums\n" + " MAC Address: %s\n" + " Firmware version: %s", this->throttle_, const_cast(this->mac_.c_str()), const_cast(this->version_.c_str())); } @@ -262,8 +263,7 @@ bool LD2450Component::get_timeout_status_(uint32_t check_millis) { if (this->timeout_ == 0) { this->timeout_ = ld2450::convert_seconds_to_ms(DEFAULT_PRESENCE_TIMEOUT); } - auto current_millis = millis(); - return current_millis - check_millis >= this->timeout_; + return App.get_loop_component_start_time() - check_millis >= this->timeout_; } // Extract, store and publish zone details LD2450 buffer @@ -350,25 +350,24 @@ void LD2450Component::send_command_(uint8_t command, const uint8_t *command_valu // Header Target 1 Target 2 Target 3 End void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { if (len < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes) - ESP_LOGE(TAG, "Periodic data: invalid message length"); + ESP_LOGE(TAG, "Invalid message length"); return; } if (buffer[0] != 0xAA || buffer[1] != 0xFF || buffer[2] != 0x03 || buffer[3] != 0x00) { // header - ESP_LOGE(TAG, "Periodic data: invalid message header"); + ESP_LOGE(TAG, "Invalid message header"); return; } if (buffer[len - 2] != 0x55 || buffer[len - 1] != 0xCC) { // footer - ESP_LOGE(TAG, "Periodic data: invalid message footer"); + ESP_LOGE(TAG, "Invalid message footer"); return; } - auto current_millis = millis(); - if (current_millis - this->last_periodic_millis_ < this->throttle_) { + if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) { ESP_LOGV(TAG, "Throttling: %d", this->throttle_); return; } - this->last_periodic_millis_ = current_millis; + this->last_periodic_millis_ = App.get_loop_component_start_time(); int16_t target_count = 0; int16_t still_target_count = 0; @@ -551,13 +550,13 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { #ifdef USE_SENSOR // For presence timeout check if (target_count > 0) { - this->presence_millis_ = millis(); + this->presence_millis_ = App.get_loop_component_start_time(); } if (moving_target_count > 0) { - this->moving_presence_millis_ = millis(); + this->moving_presence_millis_ = App.get_loop_component_start_time(); } if (still_target_count > 0) { - this->still_presence_millis_ = millis(); + this->still_presence_millis_ = App.get_loop_component_start_time(); } #endif } @@ -565,31 +564,31 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { ESP_LOGV(TAG, "Handling ack data for command %02X", buffer[COMMAND]); if (len < 10) { - ESP_LOGE(TAG, "Ack data: invalid length"); + ESP_LOGE(TAG, "Invalid ack length"); return true; } if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) { // frame header - ESP_LOGE(TAG, "Ack data: invalid header (command %02X)", buffer[COMMAND]); + ESP_LOGE(TAG, "Invalid ack header (command %02X)", buffer[COMMAND]); return true; } if (buffer[COMMAND_STATUS] != 0x01) { - ESP_LOGE(TAG, "Ack data: invalid status"); + ESP_LOGE(TAG, "Invalid ack status"); return true; } if (buffer[8] || buffer[9]) { - ESP_LOGE(TAG, "Ack data: last buffer was %u, %u", buffer[8], buffer[9]); + ESP_LOGE(TAG, "Last buffer was %u, %u", buffer[8], buffer[9]); return true; } switch (buffer[COMMAND]) { case lowbyte(CMD_ENABLE_CONF): - ESP_LOGV(TAG, "Got enable conf command"); + ESP_LOGV(TAG, "Enable conf command"); break; case lowbyte(CMD_DISABLE_CONF): - ESP_LOGV(TAG, "Got disable conf command"); + ESP_LOGV(TAG, "Disable conf command"); break; case lowbyte(CMD_SET_BAUD_RATE): - ESP_LOGV(TAG, "Got baud rate change command"); + ESP_LOGV(TAG, "Baud rate change command"); #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { ESP_LOGV(TAG, "Change baud rate to %s", this->baud_rate_select_->state.c_str()); @@ -623,10 +622,10 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { #endif break; case lowbyte(CMD_BLUETOOTH): - ESP_LOGV(TAG, "Got Bluetooth command"); + ESP_LOGV(TAG, "Bluetooth command"); break; case lowbyte(CMD_SINGLE_TARGET_MODE): - ESP_LOGV(TAG, "Got single target conf command"); + ESP_LOGV(TAG, "Single target conf command"); #ifdef USE_SWITCH if (this->multi_target_switch_ != nullptr) { this->multi_target_switch_->publish_state(false); @@ -634,7 +633,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { #endif break; case lowbyte(CMD_MULTI_TARGET_MODE): - ESP_LOGV(TAG, "Got multi target conf command"); + ESP_LOGV(TAG, "Multi target conf command"); #ifdef USE_SWITCH if (this->multi_target_switch_ != nullptr) { this->multi_target_switch_->publish_state(true); @@ -642,7 +641,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { #endif break; case lowbyte(CMD_QUERY_TARGET_MODE): - ESP_LOGV(TAG, "Got query target tracking mode command"); + ESP_LOGV(TAG, "Query target tracking mode command"); #ifdef USE_SWITCH if (this->multi_target_switch_ != nullptr) { this->multi_target_switch_->publish_state(buffer[10] == 0x02); @@ -650,7 +649,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { #endif break; case lowbyte(CMD_QUERY_ZONE): - ESP_LOGV(TAG, "Got query zone conf command"); + ESP_LOGV(TAG, "Query zone conf command"); this->zone_type_ = std::stoi(std::to_string(buffer[10]), nullptr, 16); this->publish_zone_type(); #ifdef USE_SELECT @@ -670,7 +669,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { this->process_zone_(buffer); break; case lowbyte(CMD_SET_ZONE): - ESP_LOGV(TAG, "Got set zone conf command"); + ESP_LOGV(TAG, "Set zone conf command"); this->query_zone_info(); break; default: From 47db5e26f3b2ff41c831ae3ad2d2b8f74652c9e9 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 25 Jun 2025 03:16:05 -0500 Subject: [PATCH 109/293] [ld2420] Shorten log messages + other clean-up (#9200) --- esphome/components/ld2420/ld2420.cpp | 121 +++++++++++++-------------- esphome/components/ld2420/ld2420.h | 4 +- 2 files changed, 61 insertions(+), 64 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 5b3206bf12..62f1685598 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -1,4 +1,5 @@ #include "ld2420.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" /* @@ -40,7 +41,7 @@ There are three documented parameters for modes: 00 04 = Energy output mode This mode outputs detailed signal energy values for each gate and the target distance. The data format consist of the following. - Header HH, Length LL, Persence PP, Distance DD, 16 Gate Energies EE, Footer FF + Header HH, Length LL, Presence PP, Distance DD, 16 Gate Energies EE, Footer FF HH HH HH HH LL LL PP DD DD EE EE .. 16x .. FF FF FF FF F4 F3 F2 F1 23 00 00 00 00 00 00 .. .. .. .. F8 F7 F6 F5 00 00 = debug output mode @@ -67,10 +68,10 @@ float LD2420Component::get_setup_priority() const { return setup_priority::BUS; void LD2420Component::dump_config() { ESP_LOGCONFIG(TAG, "LD2420:\n" - " Firmware Version : %7s\n" - "LD2420 Number:", + " Firmware version: %7s", this->ld2420_firmware_ver_); #ifdef USE_NUMBER + ESP_LOGCONFIG(TAG, "Number:"); LOG_NUMBER(TAG, " Gate Timeout:", this->gate_timeout_number_); LOG_NUMBER(TAG, " Gate Max Distance:", this->max_gate_distance_number_); LOG_NUMBER(TAG, " Gate Min Distance:", this->min_gate_distance_number_); @@ -86,10 +87,10 @@ void LD2420Component::dump_config() { LOG_BUTTON(TAG, " Factory Reset:", this->factory_reset_button_); LOG_BUTTON(TAG, " Restart Module:", this->restart_module_button_); #endif - ESP_LOGCONFIG(TAG, "LD2420 Select:"); + ESP_LOGCONFIG(TAG, "Select:"); LOG_SELECT(TAG, " Operating Mode", this->operating_selector_); - if (this->get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { - ESP_LOGW(TAG, "LD2420 Firmware Version %s and older are only supported in Simple Mode", ld2420_firmware_ver_); + if (LD2420Component::get_firmware_int(this->ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { + ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->ld2420_firmware_ver_); } } @@ -102,7 +103,7 @@ uint8_t LD2420Component::calc_checksum(void *data, size_t size) { return checksum; } -int LD2420Component::get_firmware_int_(const char *version_string) { +int LD2420Component::get_firmware_int(const char *version_string) { std::string version_str = version_string; if (version_str[0] == 'v') { version_str = version_str.substr(1); @@ -115,7 +116,7 @@ int LD2420Component::get_firmware_int_(const char *version_string) { void LD2420Component::setup() { ESP_LOGCONFIG(TAG, "Running setup"); if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { - ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections."); + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); this->mark_failed(); return; } @@ -127,7 +128,7 @@ void LD2420Component::setup() { const char *pfw = this->ld2420_firmware_ver_; std::string fw_str(pfw); - for (auto &listener : listeners_) { + for (auto &listener : this->listeners_) { listener->on_fw_version(fw_str); } @@ -137,11 +138,11 @@ void LD2420Component::setup() { } memcpy(&this->new_config, &this->current_config, sizeof(this->current_config)); - if (get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { + if (LD2420Component::get_firmware_int(this->ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { this->set_operating_mode(OP_SIMPLE_MODE_STRING); this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING); this->set_mode_(CMD_SYSTEM_MODE_SIMPLE); - ESP_LOGW(TAG, "LD2420 Frimware Version %s and older are only supported in Simple Mode", ld2420_firmware_ver_); + ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->ld2420_firmware_ver_); } else { this->set_mode_(CMD_SYSTEM_MODE_ENERGY); this->operating_selector_->publish_state(OP_NORMAL_MODE_STRING); @@ -151,18 +152,17 @@ void LD2420Component::setup() { #endif this->set_system_mode(this->system_mode_); this->set_config_mode(false); - ESP_LOGCONFIG(TAG, "LD2420 setup complete."); } void LD2420Component::apply_config_action() { const uint8_t checksum = calc_checksum(&this->new_config, sizeof(this->new_config)); if (checksum == calc_checksum(&this->current_config, sizeof(this->current_config))) { - ESP_LOGCONFIG(TAG, "No configuration change detected"); + ESP_LOGD(TAG, "No configuration change detected"); return; } - ESP_LOGCONFIG(TAG, "Reconfiguring LD2420"); + ESP_LOGD(TAG, "Reconfiguring"); if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { - ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections."); + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); this->mark_failed(); return; } @@ -178,13 +178,12 @@ void LD2420Component::apply_config_action() { this->set_system_mode(this->system_mode_); this->set_config_mode(false); // Disable config mode to save new values in LD2420 nvm this->set_operating_mode(OP_NORMAL_MODE_STRING); - ESP_LOGCONFIG(TAG, "LD2420 reconfig complete."); } void LD2420Component::factory_reset_action() { - ESP_LOGCONFIG(TAG, "Setting factory defaults"); + ESP_LOGD(TAG, "Setting factory defaults"); if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { - ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections."); + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); this->mark_failed(); return; } @@ -207,18 +206,16 @@ void LD2420Component::factory_reset_action() { this->init_gate_config_numbers(); this->refresh_gate_config_numbers(); #endif - ESP_LOGCONFIG(TAG, "LD2420 factory reset complete."); } void LD2420Component::restart_module_action() { - ESP_LOGCONFIG(TAG, "Restarting LD2420 module"); + ESP_LOGD(TAG, "Restarting"); this->send_module_restart(); this->set_timeout(250, [this]() { this->set_config_mode(true); - this->set_system_mode(system_mode_); + this->set_system_mode(this->system_mode_); this->set_config_mode(false); }); - ESP_LOGCONFIG(TAG, "LD2420 Restarted."); } void LD2420Component::revert_config_action() { @@ -226,18 +223,18 @@ void LD2420Component::revert_config_action() { #ifdef USE_NUMBER this->init_gate_config_numbers(); #endif - ESP_LOGCONFIG(TAG, "Reverted config number edits."); + ESP_LOGD(TAG, "Reverted config number edits"); } void LD2420Component::loop() { // If there is a active send command do not process it here, the send command call will handle it. - if (!get_cmd_active_()) { - if (!available()) + if (!this->get_cmd_active_()) { + if (!this->available()) return; static uint8_t buffer[2048]; static uint8_t rx_data; - while (available()) { - rx_data = read(); + while (this->available()) { + rx_data = this->read(); this->readline_(rx_data, buffer, sizeof(buffer)); } } @@ -292,7 +289,7 @@ void LD2420Component::report_gate_data() { void LD2420Component::set_operating_mode(const std::string &state) { // If unsupported firmware ignore mode select - if (get_firmware_int_(ld2420_firmware_ver_) >= CALIBRATE_VERSION_MIN) { + if (LD2420Component::get_firmware_int(ld2420_firmware_ver_) >= CALIBRATE_VERSION_MIN) { this->current_operating_mode = OP_MODE_TO_UINT.at(state); // Entering Auto Calibrate we need to clear the privoiuos data collection this->operating_selector_->publish_state(state); @@ -365,13 +362,13 @@ void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) { } // Resonable refresh rate for home assistant database size health - const int32_t current_millis = millis(); + const int32_t current_millis = App.get_loop_component_start_time(); if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS) return; this->last_periodic_millis = current_millis; for (auto &listener : this->listeners_) { - listener->on_distance(get_distance_()); - listener->on_presence(get_presence_()); + listener->on_distance(this->get_distance_()); + listener->on_presence(this->get_presence_()); listener->on_energy(this->gate_energy_, sizeof(this->gate_energy_) / sizeof(this->gate_energy_[0])); } @@ -392,9 +389,9 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) { char outbuf[bufsize]{0}; while (true) { if (inbuf[pos - 2] == 'O' && inbuf[pos - 1] == 'F' && inbuf[pos] == 'F') { - set_presence_(false); + this->set_presence_(false); } else if (inbuf[pos - 1] == 'O' && inbuf[pos] == 'N') { - set_presence_(true); + this->set_presence_(true); } if (inbuf[pos] >= '0' && inbuf[pos] <= '9') { if (index < bufsize - 1) { @@ -411,18 +408,18 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) { } outbuf[index] = '\0'; if (index > 1) - set_distance_(strtol(outbuf, &endptr, 10)); + this->set_distance_(strtol(outbuf, &endptr, 10)); - if (get_mode_() == CMD_SYSTEM_MODE_SIMPLE) { + if (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE) { // Resonable refresh rate for home assistant database size health - const int32_t current_millis = millis(); + const int32_t current_millis = App.get_loop_component_start_time(); if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS) return; this->last_normal_periodic_millis = current_millis; for (auto &listener : this->listeners_) - listener->on_distance(get_distance_()); + listener->on_distance(this->get_distance_()); for (auto &listener : this->listeners_) - listener->on_presence(get_presence_()); + listener->on_presence(this->get_presence_()); } } @@ -433,10 +430,10 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { uint8_t data_element = 0; uint16_t data_pos = 0; if (this->cmd_reply_.length > CMD_MAX_BYTES) { - ESP_LOGW(TAG, "LD2420 reply - received command reply frame is corrupt, length exceeds %d bytes.", CMD_MAX_BYTES); + ESP_LOGW(TAG, "Reply frame too long"); return; } else if (this->cmd_reply_.length < 2) { - ESP_LOGW(TAG, "LD2420 reply - received command frame is corrupt, length is less than 2 bytes."); + ESP_LOGW(TAG, "Command frame too short"); return; } memcpy(&this->cmd_reply_.error, &buffer[CMD_ERROR_WORD], sizeof(this->cmd_reply_.error)); @@ -447,13 +444,13 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { this->cmd_reply_.ack = true; switch ((uint16_t) this->cmd_reply_.command) { case (CMD_ENABLE_CONF): - ESP_LOGD(TAG, "LD2420 reply - set config enable: CMD = %2X %s", CMD_ENABLE_CONF, result); + ESP_LOGV(TAG, "Set config enable: CMD = %2X %s", CMD_ENABLE_CONF, result); break; case (CMD_DISABLE_CONF): - ESP_LOGD(TAG, "LD2420 reply - set config disable: CMD = %2X %s", CMD_DISABLE_CONF, result); + ESP_LOGV(TAG, "Set config disable: CMD = %2X %s", CMD_DISABLE_CONF, result); break; case (CMD_READ_REGISTER): - ESP_LOGD(TAG, "LD2420 reply - read register: CMD = %2X %s", CMD_READ_REGISTER, result); + ESP_LOGV(TAG, "Read register: CMD = %2X %s", CMD_READ_REGISTER, result); // TODO Read/Write register is not implemented yet, this will get flushed out to a proper header file data_pos = 0x0A; for (uint16_t index = 0; index < (CMD_REG_DATA_REPLY_SIZE * // NOLINT @@ -465,13 +462,13 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { } break; case (CMD_WRITE_REGISTER): - ESP_LOGD(TAG, "LD2420 reply - write register: CMD = %2X %s", CMD_WRITE_REGISTER, result); + ESP_LOGV(TAG, "Write register: CMD = %2X %s", CMD_WRITE_REGISTER, result); break; case (CMD_WRITE_ABD_PARAM): - ESP_LOGD(TAG, "LD2420 reply - write gate parameter(s): %2X %s", CMD_WRITE_ABD_PARAM, result); + ESP_LOGV(TAG, "Write gate parameter(s): %2X %s", CMD_WRITE_ABD_PARAM, result); break; case (CMD_READ_ABD_PARAM): - ESP_LOGD(TAG, "LD2420 reply - read gate parameter(s): %2X %s", CMD_READ_ABD_PARAM, result); + ESP_LOGV(TAG, "Read gate parameter(s): %2X %s", CMD_READ_ABD_PARAM, result); data_pos = CMD_ABD_DATA_REPLY_START; for (uint16_t index = 0; index < (CMD_ABD_DATA_REPLY_SIZE * // NOLINT ((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_ABD_DATA_REPLY_SIZE)); @@ -483,11 +480,11 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { } break; case (CMD_WRITE_SYS_PARAM): - ESP_LOGD(TAG, "LD2420 reply - set system parameter(s): %2X %s", CMD_WRITE_SYS_PARAM, result); + ESP_LOGV(TAG, "Set system parameter(s): %2X %s", CMD_WRITE_SYS_PARAM, result); break; case (CMD_READ_VERSION): memcpy(this->ld2420_firmware_ver_, &buffer[12], buffer[10]); - ESP_LOGD(TAG, "LD2420 reply - module firmware version: %7s %s", this->ld2420_firmware_ver_, result); + ESP_LOGV(TAG, "Firmware version: %7s %s", this->ld2420_firmware_ver_, result); break; default: break; @@ -533,7 +530,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) { } while (!this->cmd_reply_.ack) { - while (available()) { + while (this->available()) { this->readline_(read(), ack_buffer, sizeof(ack_buffer)); } delay_microseconds_safe(1450); @@ -548,7 +545,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) { if (this->cmd_reply_.ack) retry = 0; if (this->cmd_reply_.error > 0) - handle_cmd_error(error); + this->handle_cmd_error(error); } return error; } @@ -563,7 +560,7 @@ uint8_t LD2420Component::set_config_mode(bool enable) { cmd_frame.data_length += sizeof(CMD_PROTOCOL_VER); } cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending set config %s command: %2X", enable ? "enable" : "disable", cmd_frame.command); + ESP_LOGV(TAG, "Sending set config %s command: %2X", enable ? "enable" : "disable", cmd_frame.command); return this->send_cmd_from_array(cmd_frame); } @@ -576,7 +573,7 @@ void LD2420Component::ld2420_restart() { cmd_frame.header = CMD_FRAME_HEADER; cmd_frame.command = CMD_RESTART; cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending restart command: %2X", cmd_frame.command); + ESP_LOGV(TAG, "Sending restart command: %2X", cmd_frame.command); this->send_cmd_from_array(cmd_frame); } @@ -588,7 +585,7 @@ void LD2420Component::get_reg_value_(uint16_t reg) { cmd_frame.data[1] = reg; cmd_frame.data_length += 2; cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending read register %4X command: %2X", reg, cmd_frame.command); + ESP_LOGV(TAG, "Sending read register %4X command: %2X", reg, cmd_frame.command); this->send_cmd_from_array(cmd_frame); } @@ -602,11 +599,11 @@ void LD2420Component::set_reg_value(uint16_t reg, uint16_t value) { memcpy(&cmd_frame.data[cmd_frame.data_length], &value, sizeof(CMD_REG_DATA_REPLY_SIZE)); cmd_frame.data_length += 2; cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending write register %4X command: %2X data = %4X", reg, cmd_frame.command, value); + ESP_LOGV(TAG, "Sending write register %4X command: %2X data = %4X", reg, cmd_frame.command, value); this->send_cmd_from_array(cmd_frame); } -void LD2420Component::handle_cmd_error(uint8_t error) { ESP_LOGI(TAG, "Command failed: %s", ERR_MESSAGE[error]); } +void LD2420Component::handle_cmd_error(uint8_t error) { ESP_LOGE(TAG, "Command failed: %s", ERR_MESSAGE[error]); } int LD2420Component::get_gate_threshold_(uint8_t gate) { uint8_t error; @@ -619,7 +616,7 @@ int LD2420Component::get_gate_threshold_(uint8_t gate) { memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_GATE_STILL_THRESH[gate], sizeof(CMD_GATE_STILL_THRESH[gate])); cmd_frame.data_length += 2; cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending read gate %d high/low theshold command: %2X", gate, cmd_frame.command); + ESP_LOGV(TAG, "Sending read gate %d high/low threshold command: %2X", gate, cmd_frame.command); error = this->send_cmd_from_array(cmd_frame); if (error == 0) { this->current_config.move_thresh[gate] = cmd_reply_.data[0]; @@ -644,7 +641,7 @@ int LD2420Component::get_min_max_distances_timeout_() { sizeof(CMD_TIMEOUT_REG)); // Register: global delay time cmd_frame.data_length += sizeof(CMD_TIMEOUT_REG); cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending read gate min max and timeout command: %2X", cmd_frame.command); + ESP_LOGV(TAG, "Sending read gate min max and timeout command: %2X", cmd_frame.command); error = this->send_cmd_from_array(cmd_frame); if (error == 0) { this->current_config.min_gate = (uint16_t) cmd_reply_.data[0]; @@ -667,9 +664,9 @@ void LD2420Component::set_system_mode(uint16_t mode) { memcpy(&cmd_frame.data[cmd_frame.data_length], &unknown_parm, sizeof(unknown_parm)); cmd_frame.data_length += sizeof(unknown_parm); cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending write system mode command: %2X", cmd_frame.command); + ESP_LOGV(TAG, "Sending write system mode command: %2X", cmd_frame.command); if (this->send_cmd_from_array(cmd_frame) == 0) - set_mode_(mode); + this->set_mode_(mode); } void LD2420Component::get_firmware_version_() { @@ -679,7 +676,7 @@ void LD2420Component::get_firmware_version_() { cmd_frame.command = CMD_READ_VERSION; cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending read firmware version command: %2X", cmd_frame.command); + ESP_LOGV(TAG, "Sending read firmware version command: %2X", cmd_frame.command); this->send_cmd_from_array(cmd_frame); } @@ -712,7 +709,7 @@ void LD2420Component::set_min_max_distances_timeout(uint32_t max_gate_distance, cmd_frame.data_length += sizeof(timeout); cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending write gate min max and timeout command: %2X", cmd_frame.command); + ESP_LOGV(TAG, "Sending write gate min max and timeout command: %2X", cmd_frame.command); this->send_cmd_from_array(cmd_frame); } @@ -738,7 +735,7 @@ void LD2420Component::set_gate_threshold(uint8_t gate) { sizeof(this->new_config.still_thresh[gate])); cmd_frame.data_length += sizeof(this->new_config.still_thresh[gate]); cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending set gate %4X sensitivity command: %2X", gate, cmd_frame.command); + ESP_LOGV(TAG, "Sending set gate %4X sensitivity command: %2X", gate, cmd_frame.command); this->send_cmd_from_array(cmd_frame); } diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 2b50c7a1d4..5e011100e6 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -179,7 +179,7 @@ class LD2420Component : public Component, public uart::UARTDevice { void set_operating_mode(const std::string &state); void auto_calibrate_sensitivity(); void update_radar_data(uint16_t const *gate_energy, uint8_t sample_number); - uint8_t calc_checksum(void *data, size_t size); + static uint8_t calc_checksum(void *data, size_t size); RegConfigT current_config; RegConfigT new_config; @@ -222,7 +222,7 @@ class LD2420Component : public Component, public uart::UARTDevice { volatile bool ack; }; - int get_firmware_int_(const char *version_string); + static int get_firmware_int(const char *version_string); void get_firmware_version_(); int get_gate_threshold_(uint8_t gate); void get_reg_value_(uint16_t reg); From 5531296ee092ae33c20fd318c396711c892fd5fe Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 25 Jun 2025 04:48:32 -0500 Subject: [PATCH 110/293] [ld2410] Use ``App.get_loop_component_start_time()``, shorten log messages (#9194) Co-authored-by: J. Nick Koston --- esphome/components/ld2410/ld2410.cpp | 50 +++++++++++++++------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 4272159da6..b3c3649ceb 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -8,6 +8,8 @@ #include "esphome/components/sensor/sensor.h" #endif +#include "esphome/core/application.h" + #define highbyte(val) (uint8_t)((val) >> 8) #define lowbyte(val) (uint8_t)((val) &0xff) @@ -73,9 +75,9 @@ void LD2410Component::dump_config() { #endif this->read_all_info(); ESP_LOGCONFIG(TAG, - " Throttle_ : %ums\n" - " MAC Address : %s\n" - " Firmware Version : %s", + " Throttle: %ums\n" + " MAC address: %s\n" + " Firmware version: %s", this->throttle_, const_cast(this->mac_.c_str()), const_cast(this->version_.c_str())); } @@ -153,7 +155,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { /* Reduce data update rate to prevent home assistant database size grow fast */ - int32_t current_millis = millis(); + int32_t current_millis = App.get_loop_component_start_time(); if (current_millis - last_periodic_millis_ < this->throttle_) return; last_periodic_millis_ = current_millis; @@ -313,40 +315,40 @@ std::function set_number_value(number::Number *n, float value) { bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", buffer[COMMAND]); if (len < 10) { - ESP_LOGE(TAG, "Error with last command : incorrect length"); + ESP_LOGE(TAG, "Invalid length"); return true; } if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) { // check 4 frame start bytes - ESP_LOGE(TAG, "Error with last command : incorrect Header"); + ESP_LOGE(TAG, "Invalid header"); return true; } if (buffer[COMMAND_STATUS] != 0x01) { - ESP_LOGE(TAG, "Error with last command : status != 0x01"); + ESP_LOGE(TAG, "Invalid status"); return true; } if (this->two_byte_to_int_(buffer[8], buffer[9]) != 0x00) { - ESP_LOGE(TAG, "Error with last command , last buffer was: %u , %u", buffer[8], buffer[9]); + ESP_LOGE(TAG, "Invalid command: %u, %u", buffer[8], buffer[9]); return true; } switch (buffer[COMMAND]) { case lowbyte(CMD_ENABLE_CONF): - ESP_LOGV(TAG, "Handled Enable conf command"); + ESP_LOGV(TAG, "Enable conf"); break; case lowbyte(CMD_DISABLE_CONF): - ESP_LOGV(TAG, "Handled Disabled conf command"); + ESP_LOGV(TAG, "Disabled conf"); break; case lowbyte(CMD_SET_BAUD_RATE): - ESP_LOGV(TAG, "Handled baud rate change command"); + ESP_LOGV(TAG, "Baud rate change"); #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { - ESP_LOGE(TAG, "Change baud rate component config to %s and reinstall", this->baud_rate_select_->state.c_str()); + ESP_LOGE(TAG, "Configure baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); } #endif break; case lowbyte(CMD_VERSION): this->version_ = format_version(buffer); - ESP_LOGV(TAG, "FW Version is: %s", const_cast(this->version_.c_str())); + ESP_LOGV(TAG, "Firmware version: %s", const_cast(this->version_.c_str())); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { this->version_text_sensor_->publish_state(this->version_); @@ -356,7 +358,7 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { case lowbyte(CMD_QUERY_DISTANCE_RESOLUTION): { std::string distance_resolution = DISTANCE_RESOLUTION_INT_TO_ENUM.at(this->two_byte_to_int_(buffer[10], buffer[11])); - ESP_LOGV(TAG, "Distance resolution is: %s", const_cast(distance_resolution.c_str())); + ESP_LOGV(TAG, "Distance resolution: %s", const_cast(distance_resolution.c_str())); #ifdef USE_SELECT if (this->distance_resolution_select_ != nullptr && this->distance_resolution_select_->state != distance_resolution) { @@ -368,9 +370,9 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { this->light_function_ = LIGHT_FUNCTION_INT_TO_ENUM.at(buffer[10]); this->light_threshold_ = buffer[11] * 1.0; this->out_pin_level_ = OUT_PIN_LEVEL_INT_TO_ENUM.at(buffer[12]); - ESP_LOGV(TAG, "Light function is: %s", const_cast(this->light_function_.c_str())); - ESP_LOGV(TAG, "Light threshold is: %f", this->light_threshold_); - ESP_LOGV(TAG, "Out pin level is: %s", const_cast(this->out_pin_level_.c_str())); + ESP_LOGV(TAG, "Light function: %s", const_cast(this->light_function_.c_str())); + ESP_LOGV(TAG, "Light threshold: %f", this->light_threshold_); + ESP_LOGV(TAG, "Out pin level: %s", const_cast(this->out_pin_level_.c_str())); #ifdef USE_SELECT if (this->light_function_select_ != nullptr && this->light_function_select_->state != this->light_function_) { this->light_function_select_->publish_state(this->light_function_); @@ -405,19 +407,19 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { #endif break; case lowbyte(CMD_GATE_SENS): - ESP_LOGV(TAG, "Handled sensitivity command"); + ESP_LOGV(TAG, "Sensitivity"); break; case lowbyte(CMD_BLUETOOTH): - ESP_LOGV(TAG, "Handled bluetooth command"); + ESP_LOGV(TAG, "Bluetooth"); break; case lowbyte(CMD_SET_DISTANCE_RESOLUTION): - ESP_LOGV(TAG, "Handled set distance resolution command"); + ESP_LOGV(TAG, "Set distance resolution"); break; case lowbyte(CMD_SET_LIGHT_CONTROL): - ESP_LOGV(TAG, "Handled set light control command"); + ESP_LOGV(TAG, "Set light control"); break; case lowbyte(CMD_BT_PASSWORD): - ESP_LOGV(TAG, "Handled set bluetooth password command"); + ESP_LOGV(TAG, "Set bluetooth password"); break; case lowbyte(CMD_QUERY): // Query parameters response { @@ -517,7 +519,7 @@ void LD2410Component::set_baud_rate(const std::string &state) { void LD2410Component::set_bluetooth_password(const std::string &password) { if (password.length() != 6) { - ESP_LOGE(TAG, "set_bluetooth_password(): invalid password length, must be exactly 6 chars '%s'", password.c_str()); + ESP_LOGE(TAG, "Password must be exactly 6 chars"); return; } this->set_config_mode_(true); @@ -529,7 +531,7 @@ void LD2410Component::set_bluetooth_password(const std::string &password) { void LD2410Component::set_engineering_mode(bool enable) { this->set_config_mode_(true); - last_engineering_mode_change_millis_ = millis(); + last_engineering_mode_change_millis_ = App.get_loop_component_start_time(); uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG; this->send_command_(cmd, nullptr, 0); this->set_config_mode_(false); From 5362d1a89f19df22611413bb45a5d28cfe2e6abc Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Jun 2025 05:49:31 -0400 Subject: [PATCH 111/293] [esp32_hall] Add dummy component (#9125) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/esp32_hall/__init__.py | 0 esphome/components/esp32_hall/sensor.py | 5 +++++ 2 files changed, 5 insertions(+) create mode 100644 esphome/components/esp32_hall/__init__.py create mode 100644 esphome/components/esp32_hall/sensor.py diff --git a/esphome/components/esp32_hall/__init__.py b/esphome/components/esp32_hall/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/esp32_hall/sensor.py b/esphome/components/esp32_hall/sensor.py new file mode 100644 index 0000000000..b644389d3b --- /dev/null +++ b/esphome/components/esp32_hall/sensor.py @@ -0,0 +1,5 @@ +import esphome.config_validation as cv + +CONFIG_SCHEMA = cv.invalid( + "The esp32_hall component has been removed as of ESPHome 2025.7.0. See https://github.com/esphome/esphome/pull/9117 for details." +) From 16860e8a30fee6d4f986718f430f45533df48ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Wed, 25 Jun 2025 10:20:29 +0000 Subject: [PATCH 112/293] fix(MQTT): Call disconnect callback on DNS error (#9016) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/mqtt/mqtt_backend.h | 3 ++- esphome/components/mqtt/mqtt_client.cpp | 4 ++++ esphome/components/mqtt/mqtt_client.h | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_backend.h b/esphome/components/mqtt/mqtt_backend.h index 3962c40a42..0c1720ec34 100644 --- a/esphome/components/mqtt/mqtt_backend.h +++ b/esphome/components/mqtt/mqtt_backend.h @@ -17,7 +17,8 @@ enum class MQTTClientDisconnectReason : int8_t { MQTT_MALFORMED_CREDENTIALS = 4, MQTT_NOT_AUTHORIZED = 5, ESP8266_NOT_ENOUGH_SPACE = 6, - TLS_BAD_FINGERPRINT = 7 + TLS_BAD_FINGERPRINT = 7, + DNS_RESOLVE_ERROR = 8 }; /// internal struct for MQTT messages. diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 3ba1ac6077..20e0b4a499 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -229,6 +229,8 @@ void MQTTClientComponent::check_dnslookup_() { if (this->dns_resolve_error_) { ESP_LOGW(TAG, "Couldn't resolve IP address for '%s'", this->credentials_.address.c_str()); this->state_ = MQTT_CLIENT_DISCONNECTED; + this->disconnect_reason_ = MQTTClientDisconnectReason::DNS_RESOLVE_ERROR; + this->on_disconnect_.call(MQTTClientDisconnectReason::DNS_RESOLVE_ERROR); return; } @@ -698,7 +700,9 @@ void MQTTClientComponent::set_on_connect(mqtt_on_connect_callback_t &&callback) } void MQTTClientComponent::set_on_disconnect(mqtt_on_disconnect_callback_t &&callback) { + auto callback_copy = callback; this->mqtt_backend_.set_on_disconnect(std::forward(callback)); + this->on_disconnect_.add(std::move(callback_copy)); } #if ASYNC_TCP_SSL_ENABLED diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index a95b122383..325ca56f4b 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -8,6 +8,7 @@ #include "esphome/components/network/ip_address.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #if defined(USE_ESP32) #include "mqtt_backend_esp32.h" @@ -334,6 +335,7 @@ class MQTTClientComponent : public Component { uint32_t connect_begin_; uint32_t last_connected_{0}; optional disconnect_reason_{}; + CallbackManager on_disconnect_; bool publish_nan_as_none_{false}; bool wait_for_connection_{false}; From 7c2813421411e55c188b177df9ca105c383e43be Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Wed, 25 Jun 2025 13:36:24 +0300 Subject: [PATCH 113/293] Rename kVARh/VARh to kvarh/varh (#9191) --- esphome/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/const.py b/esphome/const.py index e61af6c5b5..5e551efc2c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1096,7 +1096,7 @@ UNIT_KILOMETER_PER_HOUR = "km/h" UNIT_KILOVOLT_AMPS = "kVA" UNIT_KILOVOLT_AMPS_HOURS = "kVAh" UNIT_KILOVOLT_AMPS_REACTIVE = "kVAR" -UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVARh" +UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kvarh" UNIT_KILOWATT = "kW" UNIT_KILOWATT_HOURS = "kWh" UNIT_LITRE = "L" @@ -1132,7 +1132,7 @@ UNIT_VOLT = "V" UNIT_VOLT_AMPS = "VA" UNIT_VOLT_AMPS_HOURS = "VAh" UNIT_VOLT_AMPS_REACTIVE = "var" -UNIT_VOLT_AMPS_REACTIVE_HOURS = "VARh" +UNIT_VOLT_AMPS_REACTIVE_HOURS = "varh" UNIT_WATT = "W" UNIT_WATT_HOURS = "Wh" From b18ff48b4ad8bd594fc8410c0f8e5b2cc2a2583c Mon Sep 17 00:00:00 2001 From: DanielV Date: Wed, 25 Jun 2025 14:03:41 +0200 Subject: [PATCH 114/293] [API] Sub devices and areas (#8544) Co-authored-by: J. Nick Koston Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .../alarm_control_panel/__init__.py | 7 +- esphome/components/api/api.proto | 41 ++ esphome/components/api/api_connection.cpp | 17 + esphome/components/api/api_connection.h | 3 + esphome/components/api/api_pb2.cpp | 388 ++++++++++++ esphome/components/api/api_pb2.h | 81 ++- esphome/components/binary_sensor/__init__.py | 7 +- esphome/components/button/__init__.py | 7 +- esphome/components/climate/__init__.py | 7 +- esphome/components/cover/__init__.py | 7 +- esphome/components/datetime/__init__.py | 6 +- esphome/components/demo/__init__.py | 4 +- esphome/components/esp32_camera/__init__.py | 4 +- esphome/components/event/__init__.py | 7 +- esphome/components/fan/__init__.py | 7 +- esphome/components/light/__init__.py | 6 +- esphome/components/lock/__init__.py | 7 +- esphome/components/media_player/__init__.py | 7 +- esphome/components/number/__init__.py | 7 +- esphome/components/select/__init__.py | 7 +- esphome/components/sensor/__init__.py | 6 +- esphome/components/switch/__init__.py | 7 +- esphome/components/text/__init__.py | 7 +- esphome/components/text_sensor/__init__.py | 7 +- esphome/components/update/__init__.py | 7 +- esphome/components/valve/__init__.py | 7 +- esphome/config_validation.py | 13 +- esphome/const.py | 3 + esphome/core/__init__.py | 4 + esphome/core/application.h | 46 +- esphome/core/area.h | 19 + esphome/core/config.py | 146 ++++- esphome/core/defines.h | 2 + esphome/core/device.h | 20 + esphome/core/entity_base.cpp | 23 +- esphome/core/entity_base.h | 18 + esphome/core/entity_helpers.py | 158 ++++- esphome/cpp_helpers.py | 22 - esphome/dashboard/util/text.py | 26 +- esphome/helpers.py | 47 ++ tests/components/ade7880/common.yaml | 38 +- .../alarm_control_panel/common.yaml | 2 +- .../components/binary_sensor_map/common.yaml | 6 +- tests/components/dallas_temp/common.yaml | 4 +- tests/components/esphome/common.yaml | 20 +- tests/components/heatpumpir/common.yaml | 6 +- tests/components/light/common.yaml | 2 +- tests/components/ltr390/common.yaml | 16 +- tests/components/lvgl/common.yaml | 14 +- tests/components/opentherm/common.yaml | 2 +- tests/components/packages/test.esp32-ard.yaml | 2 +- tests/components/packages/test.esp32-idf.yaml | 2 +- .../remote_transmitter/common-buttons.yaml | 2 +- tests/dummy_main.cpp | 2 +- tests/integration/conftest.py | 1 + .../fixtures/areas_and_devices.yaml | 57 ++ ...plicate_entities_on_different_devices.yaml | 154 +++++ tests/integration/fixtures/legacy_area.yaml | 15 + tests/integration/test_areas_and_devices.py | 121 ++++ tests/integration/test_duplicate_entities.py | 184 ++++++ tests/integration/test_legacy_area.py | 41 ++ tests/unit_tests/conftest.py | 9 + tests/unit_tests/core/__init__.py | 0 tests/unit_tests/core/common.py | 33 + tests/unit_tests/core/conftest.py | 18 + tests/unit_tests/core/test_config.py | 225 +++++++ tests/unit_tests/core/test_entity_helpers.py | 595 ++++++++++++++++++ .../core/config/area_id_collision.yaml | 10 + .../core/config/area_id_hash_collision.yaml | 10 + .../core/config/device_duplicate_id.yaml | 10 + .../core/config/device_id_collision.yaml | 10 + .../core/config/device_invalid_area.yaml | 12 + .../core/config/device_without_area.yaml | 7 + .../core/config/legacy_string_area.yaml | 5 + .../core/config/multiple_areas_devices.yaml | 22 + .../core/config/valid_area_device.yaml | 11 + .../core/entity_helpers/duplicate_entity.yaml | 13 + .../duplicate_entity_with_devices.yaml | 26 + .../entity_different_platforms.yaml | 20 + 79 files changed, 2756 insertions(+), 186 deletions(-) create mode 100644 esphome/core/area.h create mode 100644 esphome/core/device.h create mode 100644 tests/integration/fixtures/areas_and_devices.yaml create mode 100644 tests/integration/fixtures/duplicate_entities_on_different_devices.yaml create mode 100644 tests/integration/fixtures/legacy_area.yaml create mode 100644 tests/integration/test_areas_and_devices.py create mode 100644 tests/integration/test_duplicate_entities.py create mode 100644 tests/integration/test_legacy_area.py create mode 100644 tests/unit_tests/core/__init__.py create mode 100644 tests/unit_tests/core/common.py create mode 100644 tests/unit_tests/core/conftest.py create mode 100644 tests/unit_tests/core/test_config.py create mode 100644 tests/unit_tests/core/test_entity_helpers.py create mode 100644 tests/unit_tests/fixtures/core/config/area_id_collision.yaml create mode 100644 tests/unit_tests/fixtures/core/config/area_id_hash_collision.yaml create mode 100644 tests/unit_tests/fixtures/core/config/device_duplicate_id.yaml create mode 100644 tests/unit_tests/fixtures/core/config/device_id_collision.yaml create mode 100644 tests/unit_tests/fixtures/core/config/device_invalid_area.yaml create mode 100644 tests/unit_tests/fixtures/core/config/device_without_area.yaml create mode 100644 tests/unit_tests/fixtures/core/config/legacy_string_area.yaml create mode 100644 tests/unit_tests/fixtures/core/config/multiple_areas_devices.yaml create mode 100644 tests/unit_tests/fixtures/core/config/valid_area_device.yaml create mode 100644 tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity.yaml create mode 100644 tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity_with_devices.yaml create mode 100644 tests/unit_tests/fixtures/core/entity_helpers/entity_different_platforms.yaml diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index e88050132a..6d37d53a4c 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -14,8 +14,8 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@grahambrown11", "@hwstar"] IS_PLATFORM_COMPONENT = True @@ -149,6 +149,9 @@ _ALARM_CONTROL_PANEL_SCHEMA = ( ) +_ALARM_CONTROL_PANEL_SCHEMA.add_extra(entity_duplicate_validator("alarm_control_panel")) + + def alarm_control_panel_schema( class_: MockObjClass, *, @@ -190,7 +193,7 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id( async def setup_alarm_control_panel_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "alarm_control_panel") for conf in config.get(CONF_ON_STATE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index b23652a982..58a0b52555 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -188,6 +188,17 @@ message DeviceInfoRequest { // Empty } +message AreaInfo { + uint32 area_id = 1; + string name = 2; +} + +message DeviceInfo { + uint32 device_id = 1; + string name = 2; + uint32 area_id = 3; +} + message DeviceInfoResponse { option (id) = 10; option (source) = SOURCE_SERVER; @@ -236,6 +247,12 @@ message DeviceInfoResponse { // Supports receiving and saving api encryption key bool api_encryption_supported = 19; + + repeated DeviceInfo devices = 20; + repeated AreaInfo areas = 21; + + // Top-level area info to phase out suggested_area + AreaInfo area = 22; } message ListEntitiesRequest { @@ -280,6 +297,7 @@ message ListEntitiesBinarySensorResponse { bool disabled_by_default = 7; string icon = 8; EntityCategory entity_category = 9; + uint32 device_id = 10; } message BinarySensorStateResponse { option (id) = 21; @@ -315,6 +333,7 @@ message ListEntitiesCoverResponse { string icon = 10; EntityCategory entity_category = 11; bool supports_stop = 12; + uint32 device_id = 13; } enum LegacyCoverState { @@ -388,6 +407,7 @@ message ListEntitiesFanResponse { string icon = 10; EntityCategory entity_category = 11; repeated string supported_preset_modes = 12; + uint32 device_id = 13; } enum FanSpeed { FAN_SPEED_LOW = 0; @@ -471,6 +491,7 @@ message ListEntitiesLightResponse { bool disabled_by_default = 13; string icon = 14; EntityCategory entity_category = 15; + uint32 device_id = 16; } message LightStateResponse { option (id) = 24; @@ -563,6 +584,7 @@ message ListEntitiesSensorResponse { SensorLastResetType legacy_last_reset_type = 11; bool disabled_by_default = 12; EntityCategory entity_category = 13; + uint32 device_id = 14; } message SensorStateResponse { option (id) = 25; @@ -595,6 +617,7 @@ message ListEntitiesSwitchResponse { bool disabled_by_default = 7; EntityCategory entity_category = 8; string device_class = 9; + uint32 device_id = 10; } message SwitchStateResponse { option (id) = 26; @@ -632,6 +655,7 @@ message ListEntitiesTextSensorResponse { bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; + uint32 device_id = 9; } message TextSensorStateResponse { option (id) = 27; @@ -814,6 +838,7 @@ message ListEntitiesCameraResponse { bool disabled_by_default = 5; string icon = 6; EntityCategory entity_category = 7; + uint32 device_id = 8; } message CameraImageResponse { @@ -916,6 +941,7 @@ message ListEntitiesClimateResponse { bool supports_target_humidity = 23; float visual_min_humidity = 24; float visual_max_humidity = 25; + uint32 device_id = 26; } message ClimateStateResponse { option (id) = 47; @@ -999,6 +1025,7 @@ message ListEntitiesNumberResponse { string unit_of_measurement = 11; NumberMode mode = 12; string device_class = 13; + uint32 device_id = 14; } message NumberStateResponse { option (id) = 50; @@ -1039,6 +1066,7 @@ message ListEntitiesSelectResponse { repeated string options = 6; bool disabled_by_default = 7; EntityCategory entity_category = 8; + uint32 device_id = 9; } message SelectStateResponse { option (id) = 53; @@ -1081,6 +1109,7 @@ message ListEntitiesSirenResponse { bool supports_duration = 8; bool supports_volume = 9; EntityCategory entity_category = 10; + uint32 device_id = 11; } message SirenStateResponse { option (id) = 56; @@ -1144,6 +1173,7 @@ message ListEntitiesLockResponse { // Not yet implemented: string code_format = 11; + uint32 device_id = 12; } message LockStateResponse { option (id) = 59; @@ -1183,6 +1213,7 @@ message ListEntitiesButtonResponse { bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; + uint32 device_id = 9; } message ButtonCommandRequest { option (id) = 62; @@ -1238,6 +1269,8 @@ message ListEntitiesMediaPlayerResponse { bool supports_pause = 8; repeated MediaPlayerSupportedFormat supported_formats = 9; + + uint32 device_id = 10; } message MediaPlayerStateResponse { option (id) = 64; @@ -1778,6 +1811,7 @@ message ListEntitiesAlarmControlPanelResponse { uint32 supported_features = 8; bool requires_code = 9; bool requires_code_to_arm = 10; + uint32 device_id = 11; } message AlarmControlPanelStateResponse { @@ -1823,6 +1857,7 @@ message ListEntitiesTextResponse { uint32 max_length = 9; string pattern = 10; TextMode mode = 11; + uint32 device_id = 12; } message TextStateResponse { option (id) = 98; @@ -1863,6 +1898,7 @@ message ListEntitiesDateResponse { string icon = 5; bool disabled_by_default = 6; EntityCategory entity_category = 7; + uint32 device_id = 8; } message DateStateResponse { option (id) = 101; @@ -1906,6 +1942,7 @@ message ListEntitiesTimeResponse { string icon = 5; bool disabled_by_default = 6; EntityCategory entity_category = 7; + uint32 device_id = 8; } message TimeStateResponse { option (id) = 104; @@ -1952,6 +1989,7 @@ message ListEntitiesEventResponse { string device_class = 8; repeated string event_types = 9; + uint32 device_id = 10; } message EventResponse { option (id) = 108; @@ -1983,6 +2021,7 @@ message ListEntitiesValveResponse { bool assumed_state = 9; bool supports_position = 10; bool supports_stop = 11; + uint32 device_id = 12; } enum ValveOperation { @@ -2029,6 +2068,7 @@ message ListEntitiesDateTimeResponse { string icon = 5; bool disabled_by_default = 6; EntityCategory entity_category = 7; + uint32 device_id = 8; } message DateTimeStateResponse { option (id) = 113; @@ -2069,6 +2109,7 @@ message ListEntitiesUpdateResponse { bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; + uint32 device_id = 9; } message UpdateStateResponse { option (id) = 117; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ef791d462c..634174ce0a 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1629,6 +1629,23 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { #endif #ifdef USE_API_NOISE resp.api_encryption_supported = true; +#endif +#ifdef USE_DEVICES + for (auto const &device : App.get_devices()) { + DeviceInfo device_info; + device_info.device_id = device->get_device_id(); + device_info.name = device->get_name(); + device_info.area_id = device->get_area_id(); + resp.devices.push_back(device_info); + } +#endif +#ifdef USE_AREAS + for (auto const &area : App.get_areas()) { + AreaInfo area_info; + area_info.area_id = area->get_area_id(); + area_info.name = area->get_name(); + resp.areas.push_back(area_info); + } #endif return resp; } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 66b7ce38a7..da12a3e449 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -301,6 +301,9 @@ class APIConnection : public APIServerConnection { response.icon = entity->get_icon(); response.disabled_by_default = entity->is_disabled_by_default(); response.entity_category = static_cast(entity->get_entity_category()); +#ifdef USE_DEVICES + response.device_id = entity->get_device_id(); +#endif } // Helper function to fill common entity state fields diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 517b4d41b4..9793565ee5 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -812,6 +812,103 @@ void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {} #ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); } #endif +bool AreaInfo::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->area_id = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool AreaInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->name = value.as_string(); + return true; + } + default: + return false; + } +} +void AreaInfo::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint32(1, this->area_id); + buffer.encode_string(2, this->name); +} +void AreaInfo::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint32_field(total_size, 1, this->area_id, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void AreaInfo::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("AreaInfo {\n"); + out.append(" area_id: "); + sprintf(buffer, "%" PRIu32, this->area_id); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool DeviceInfo::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->device_id = value.as_uint32(); + return true; + } + case 3: { + this->area_id = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool DeviceInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->name = value.as_string(); + return true; + } + default: + return false; + } +} +void DeviceInfo::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint32(1, this->device_id); + buffer.encode_string(2, this->name); + buffer.encode_uint32(3, this->area_id); +} +void DeviceInfo::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_uint32_field(total_size, 1, this->area_id, false); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void DeviceInfo::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("DeviceInfo {\n"); + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" area_id: "); + sprintf(buffer, "%" PRIu32, this->area_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -896,6 +993,18 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v this->bluetooth_mac_address = value.as_string(); return true; } + case 20: { + this->devices.push_back(value.as_message()); + return true; + } + case 21: { + this->areas.push_back(value.as_message()); + return true; + } + case 22: { + this->area = value.as_message(); + return true; + } default: return false; } @@ -920,6 +1029,13 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(16, this->suggested_area); buffer.encode_string(18, this->bluetooth_mac_address); buffer.encode_bool(19, this->api_encryption_supported); + for (auto &it : this->devices) { + buffer.encode_message(20, it, true); + } + for (auto &it : this->areas) { + buffer.encode_message(21, it, true); + } + buffer.encode_message(22, this->area); } void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->uses_password, false); @@ -941,6 +1057,9 @@ void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 2, this->suggested_area, false); ProtoSize::add_string_field(total_size, 2, this->bluetooth_mac_address, false); ProtoSize::add_bool_field(total_size, 2, this->api_encryption_supported, false); + ProtoSize::add_repeated_message(total_size, 2, this->devices); + ProtoSize::add_repeated_message(total_size, 2, this->areas); + ProtoSize::add_message_object(total_size, 2, this->area, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoResponse::dump_to(std::string &out) const { @@ -1026,6 +1145,22 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append(" api_encryption_supported: "); out.append(YESNO(this->api_encryption_supported)); out.append("\n"); + + for (const auto &it : this->devices) { + out.append(" devices: "); + it.dump_to(out); + out.append("\n"); + } + + for (const auto &it : this->areas) { + out.append(" areas: "); + it.dump_to(out); + out.append("\n"); + } + + out.append(" area: "); + this->area.dump_to(out); + out.append("\n"); out.append("}"); } #endif @@ -1052,6 +1187,10 @@ bool ListEntitiesBinarySensorResponse::decode_varint(uint32_t field_id, ProtoVar this->entity_category = value.as_enum(); return true; } + case 10: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -1102,6 +1241,7 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(7, this->disabled_by_default); buffer.encode_string(8, this->icon); buffer.encode_enum(9, this->entity_category); + buffer.encode_uint32(10, this->device_id); } void ListEntitiesBinarySensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -1113,6 +1253,7 @@ void ListEntitiesBinarySensorResponse::calculate_size(uint32_t &total_size) cons ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { @@ -1154,6 +1295,11 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -1236,6 +1382,10 @@ bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->supports_stop = value.as_bool(); return true; } + case 13: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -1289,6 +1439,7 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(10, this->icon); buffer.encode_enum(11, this->entity_category); buffer.encode_bool(12, this->supports_stop); + buffer.encode_uint32(13, this->device_id); } void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -1303,6 +1454,7 @@ void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); ProtoSize::add_bool_field(total_size, 1, this->supports_stop, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesCoverResponse::dump_to(std::string &out) const { @@ -1356,6 +1508,11 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { out.append(" supports_stop: "); out.append(YESNO(this->supports_stop)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -1565,6 +1722,10 @@ bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value this->entity_category = value.as_enum(); return true; } + case 13: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -1620,6 +1781,7 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->supported_preset_modes) { buffer.encode_string(12, it, true); } + buffer.encode_uint32(13, this->device_id); } void ListEntitiesFanResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -1638,6 +1800,7 @@ void ListEntitiesFanResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, it, true); } } + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesFanResponse::dump_to(std::string &out) const { @@ -1694,6 +1857,11 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append("'").append(it).append("'"); out.append("\n"); } + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -1987,6 +2155,10 @@ bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->entity_category = value.as_enum(); return true; } + case 16: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -2055,6 +2227,7 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(13, this->disabled_by_default); buffer.encode_string(14, this->icon); buffer.encode_enum(15, this->entity_category); + buffer.encode_uint32(16, this->device_id); } void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -2080,6 +2253,7 @@ void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 2, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesLightResponse::dump_to(std::string &out) const { @@ -2151,6 +2325,11 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -2658,6 +2837,10 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->entity_category = value.as_enum(); return true; } + case 14: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -2716,6 +2899,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(11, this->legacy_last_reset_type); buffer.encode_bool(12, this->disabled_by_default); buffer.encode_enum(13, this->entity_category); + buffer.encode_uint32(14, this->device_id); } void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -2731,6 +2915,7 @@ void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_enum_field(total_size, 1, static_cast(this->legacy_last_reset_type), false); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSensorResponse::dump_to(std::string &out) const { @@ -2789,6 +2974,11 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -2860,6 +3050,10 @@ bool ListEntitiesSwitchResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->entity_category = value.as_enum(); return true; } + case 10: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -2910,6 +3104,7 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(7, this->disabled_by_default); buffer.encode_enum(8, this->entity_category); buffer.encode_string(9, this->device_class); + buffer.encode_uint32(10, this->device_id); } void ListEntitiesSwitchResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -2921,6 +3116,7 @@ void ListEntitiesSwitchResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSwitchResponse::dump_to(std::string &out) const { @@ -2962,6 +3158,11 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { out.append(" device_class: "); out.append("'").append(this->device_class).append("'"); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -3061,6 +3262,10 @@ bool ListEntitiesTextSensorResponse::decode_varint(uint32_t field_id, ProtoVarIn this->entity_category = value.as_enum(); return true; } + case 9: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -3110,6 +3315,7 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); buffer.encode_string(8, this->device_class); + buffer.encode_uint32(9, this->device_id); } void ListEntitiesTextSensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -3120,6 +3326,7 @@ void ListEntitiesTextSensorResponse::calculate_size(uint32_t &total_size) const ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { @@ -3157,6 +3364,11 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { out.append(" device_class: "); out.append("'").append(this->device_class).append("'"); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -3922,6 +4134,10 @@ bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->entity_category = value.as_enum(); return true; } + case 8: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -3966,6 +4182,7 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(5, this->disabled_by_default); buffer.encode_string(6, this->icon); buffer.encode_enum(7, this->entity_category); + buffer.encode_uint32(8, this->device_id); } void ListEntitiesCameraResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -3975,6 +4192,7 @@ void ListEntitiesCameraResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesCameraResponse::dump_to(std::string &out) const { @@ -4008,6 +4226,11 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -4156,6 +4379,10 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v this->supports_target_humidity = value.as_bool(); return true; } + case 26: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -4262,6 +4489,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(23, this->supports_target_humidity); buffer.encode_float(24, this->visual_min_humidity); buffer.encode_float(25, this->visual_max_humidity); + buffer.encode_uint32(26, this->device_id); } void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -4313,6 +4541,7 @@ void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 2, this->supports_target_humidity, false); ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_min_humidity != 0.0f, false); ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_max_humidity != 0.0f, false); + ProtoSize::add_uint32_field(total_size, 2, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesClimateResponse::dump_to(std::string &out) const { @@ -4436,6 +4665,11 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { sprintf(buffer, "%g", this->visual_max_humidity); out.append(buffer); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -4901,6 +5135,10 @@ bool ListEntitiesNumberResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->mode = value.as_enum(); return true; } + case 14: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -4971,6 +5209,7 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(11, this->unit_of_measurement); buffer.encode_enum(12, this->mode); buffer.encode_string(13, this->device_class); + buffer.encode_uint32(14, this->device_id); } void ListEntitiesNumberResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -4986,6 +5225,7 @@ void ListEntitiesNumberResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->unit_of_measurement, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesNumberResponse::dump_to(std::string &out) const { @@ -5046,6 +5286,11 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { out.append(" device_class: "); out.append("'").append(this->device_class).append("'"); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -5151,6 +5396,10 @@ bool ListEntitiesSelectResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->entity_category = value.as_enum(); return true; } + case 9: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -5202,6 +5451,7 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { } buffer.encode_bool(7, this->disabled_by_default); buffer.encode_enum(8, this->entity_category); + buffer.encode_uint32(9, this->device_id); } void ListEntitiesSelectResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -5216,6 +5466,7 @@ void ListEntitiesSelectResponse::calculate_size(uint32_t &total_size) const { } ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSelectResponse::dump_to(std::string &out) const { @@ -5255,6 +5506,11 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -5378,6 +5634,10 @@ bool ListEntitiesSirenResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->entity_category = value.as_enum(); return true; } + case 11: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -5431,6 +5691,7 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(8, this->supports_duration); buffer.encode_bool(9, this->supports_volume); buffer.encode_enum(10, this->entity_category); + buffer.encode_uint32(11, this->device_id); } void ListEntitiesSirenResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -5447,6 +5708,7 @@ void ListEntitiesSirenResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->supports_duration, false); ProtoSize::add_bool_field(total_size, 1, this->supports_volume, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSirenResponse::dump_to(std::string &out) const { @@ -5494,6 +5756,11 @@ void ListEntitiesSirenResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -5683,6 +5950,10 @@ bool ListEntitiesLockResponse::decode_varint(uint32_t field_id, ProtoVarInt valu this->requires_code = value.as_bool(); return true; } + case 12: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -5735,6 +6006,7 @@ void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(9, this->supports_open); buffer.encode_bool(10, this->requires_code); buffer.encode_string(11, this->code_format); + buffer.encode_uint32(12, this->device_id); } void ListEntitiesLockResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -5748,6 +6020,7 @@ void ListEntitiesLockResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->supports_open, false); ProtoSize::add_bool_field(total_size, 1, this->requires_code, false); ProtoSize::add_string_field(total_size, 1, this->code_format, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesLockResponse::dump_to(std::string &out) const { @@ -5797,6 +6070,11 @@ void ListEntitiesLockResponse::dump_to(std::string &out) const { out.append(" code_format: "); out.append("'").append(this->code_format).append("'"); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -5922,6 +6200,10 @@ bool ListEntitiesButtonResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->entity_category = value.as_enum(); return true; } + case 9: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -5971,6 +6253,7 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); buffer.encode_string(8, this->device_class); + buffer.encode_uint32(9, this->device_id); } void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -5981,6 +6264,7 @@ void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesButtonResponse::dump_to(std::string &out) const { @@ -6018,6 +6302,11 @@ void ListEntitiesButtonResponse::dump_to(std::string &out) const { out.append(" device_class: "); out.append("'").append(this->device_class).append("'"); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -6135,6 +6424,10 @@ bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarI this->supports_pause = value.as_bool(); return true; } + case 10: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -6187,6 +6480,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->supported_formats) { buffer.encode_message(9, it, true); } + buffer.encode_uint32(10, this->device_id); } void ListEntitiesMediaPlayerResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -6198,6 +6492,7 @@ void ListEntitiesMediaPlayerResponse::calculate_size(uint32_t &total_size) const ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); ProtoSize::add_bool_field(total_size, 1, this->supports_pause, false); ProtoSize::add_repeated_message(total_size, 1, this->supported_formats); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { @@ -6241,6 +6536,11 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { it.dump_to(out); out.append("\n"); } + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -8551,6 +8851,10 @@ bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, Pro this->requires_code_to_arm = value.as_bool(); return true; } + case 11: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -8598,6 +8902,7 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) cons buffer.encode_uint32(8, this->supported_features); buffer.encode_bool(9, this->requires_code); buffer.encode_bool(10, this->requires_code_to_arm); + buffer.encode_uint32(11, this->device_id); } void ListEntitiesAlarmControlPanelResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -8610,6 +8915,7 @@ void ListEntitiesAlarmControlPanelResponse::calculate_size(uint32_t &total_size) ProtoSize::add_uint32_field(total_size, 1, this->supported_features, false); ProtoSize::add_bool_field(total_size, 1, this->requires_code, false); ProtoSize::add_bool_field(total_size, 1, this->requires_code_to_arm, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { @@ -8656,6 +8962,11 @@ void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { out.append(" requires_code_to_arm: "); out.append(YESNO(this->requires_code_to_arm)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -8783,6 +9094,10 @@ bool ListEntitiesTextResponse::decode_varint(uint32_t field_id, ProtoVarInt valu this->mode = value.as_enum(); return true; } + case 12: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -8835,6 +9150,7 @@ void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(9, this->max_length); buffer.encode_string(10, this->pattern); buffer.encode_enum(11, this->mode); + buffer.encode_uint32(12, this->device_id); } void ListEntitiesTextResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -8848,6 +9164,7 @@ void ListEntitiesTextResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->max_length, false); ProtoSize::add_string_field(total_size, 1, this->pattern, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesTextResponse::dump_to(std::string &out) const { @@ -8899,6 +9216,11 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const { out.append(" mode: "); out.append(proto_enum_to_string(this->mode)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -9014,6 +9336,10 @@ bool ListEntitiesDateResponse::decode_varint(uint32_t field_id, ProtoVarInt valu this->entity_category = value.as_enum(); return true; } + case 8: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -9058,6 +9384,7 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); + buffer.encode_uint32(8, this->device_id); } void ListEntitiesDateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -9067,6 +9394,7 @@ void ListEntitiesDateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesDateResponse::dump_to(std::string &out) const { @@ -9100,6 +9428,11 @@ void ListEntitiesDateResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -9255,6 +9588,10 @@ bool ListEntitiesTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt valu this->entity_category = value.as_enum(); return true; } + case 8: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -9299,6 +9636,7 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); + buffer.encode_uint32(8, this->device_id); } void ListEntitiesTimeResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -9308,6 +9646,7 @@ void ListEntitiesTimeResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesTimeResponse::dump_to(std::string &out) const { @@ -9341,6 +9680,11 @@ void ListEntitiesTimeResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -9496,6 +9840,10 @@ bool ListEntitiesEventResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->entity_category = value.as_enum(); return true; } + case 10: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -9552,6 +9900,7 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->event_types) { buffer.encode_string(9, it, true); } + buffer.encode_uint32(10, this->device_id); } void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -9567,6 +9916,7 @@ void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, it, true); } } + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesEventResponse::dump_to(std::string &out) const { @@ -9610,6 +9960,11 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const { out.append("'").append(it).append("'"); out.append("\n"); } + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -9678,6 +10033,10 @@ bool ListEntitiesValveResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->supports_stop = value.as_bool(); return true; } + case 12: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -9730,6 +10089,7 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(9, this->assumed_state); buffer.encode_bool(10, this->supports_position); buffer.encode_bool(11, this->supports_stop); + buffer.encode_uint32(12, this->device_id); } void ListEntitiesValveResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -9743,6 +10103,7 @@ void ListEntitiesValveResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->assumed_state, false); ProtoSize::add_bool_field(total_size, 1, this->supports_position, false); ProtoSize::add_bool_field(total_size, 1, this->supports_stop, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesValveResponse::dump_to(std::string &out) const { @@ -9792,6 +10153,11 @@ void ListEntitiesValveResponse::dump_to(std::string &out) const { out.append(" supports_stop: "); out.append(YESNO(this->supports_stop)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -9923,6 +10289,10 @@ bool ListEntitiesDateTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt this->entity_category = value.as_enum(); return true; } + case 8: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -9967,6 +10337,7 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); + buffer.encode_uint32(8, this->device_id); } void ListEntitiesDateTimeResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -9976,6 +10347,7 @@ void ListEntitiesDateTimeResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { @@ -10009,6 +10381,11 @@ void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -10114,6 +10491,10 @@ bool ListEntitiesUpdateResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->entity_category = value.as_enum(); return true; } + case 9: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -10163,6 +10544,7 @@ void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); buffer.encode_string(8, this->device_class); + buffer.encode_uint32(9, this->device_id); } void ListEntitiesUpdateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -10173,6 +10555,7 @@ void ListEntitiesUpdateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesUpdateResponse::dump_to(std::string &out) const { @@ -10210,6 +10593,11 @@ void ListEntitiesUpdateResponse::dump_to(std::string &out) const { out.append(" device_class: "); out.append("'").append(this->device_class).append("'"); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 7d92125290..6a5b51d3a1 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -264,6 +264,7 @@ class InfoResponseProtoMessage : public ProtoMessage { bool disabled_by_default{false}; std::string icon{}; enums::EntityCategory entity_category{}; + uint32_t device_id{0}; protected: }; @@ -415,10 +416,39 @@ class DeviceInfoRequest : public ProtoMessage { protected: }; +class AreaInfo : public ProtoMessage { + public: + uint32_t area_id{0}; + std::string name{}; + void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class DeviceInfo : public ProtoMessage { + public: + uint32_t device_id{0}; + std::string name{}; + uint32_t area_id{0}; + void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; class DeviceInfoResponse : public ProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 10; - static constexpr uint16_t ESTIMATED_SIZE = 129; + static constexpr uint16_t ESTIMATED_SIZE = 219; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "device_info_response"; } #endif @@ -441,6 +471,9 @@ class DeviceInfoResponse : public ProtoMessage { std::string suggested_area{}; std::string bluetooth_mac_address{}; bool api_encryption_supported{false}; + std::vector devices{}; + std::vector areas{}; + AreaInfo area{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -493,7 +526,7 @@ class SubscribeStatesRequest : public ProtoMessage { class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 12; - static constexpr uint16_t ESTIMATED_SIZE = 56; + static constexpr uint16_t ESTIMATED_SIZE = 60; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_binary_sensor_response"; } #endif @@ -532,7 +565,7 @@ class BinarySensorStateResponse : public StateResponseProtoMessage { class ListEntitiesCoverResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 13; - static constexpr uint16_t ESTIMATED_SIZE = 62; + static constexpr uint16_t ESTIMATED_SIZE = 66; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_cover_response"; } #endif @@ -601,7 +634,7 @@ class CoverCommandRequest : public ProtoMessage { class ListEntitiesFanResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 14; - static constexpr uint16_t ESTIMATED_SIZE = 73; + static constexpr uint16_t ESTIMATED_SIZE = 77; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_fan_response"; } #endif @@ -679,7 +712,7 @@ class FanCommandRequest : public ProtoMessage { class ListEntitiesLightResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 15; - static constexpr uint16_t ESTIMATED_SIZE = 85; + static constexpr uint16_t ESTIMATED_SIZE = 90; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_light_response"; } #endif @@ -780,7 +813,7 @@ class LightCommandRequest : public ProtoMessage { class ListEntitiesSensorResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 16; - static constexpr uint16_t ESTIMATED_SIZE = 73; + static constexpr uint16_t ESTIMATED_SIZE = 77; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_sensor_response"; } #endif @@ -823,7 +856,7 @@ class SensorStateResponse : public StateResponseProtoMessage { class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 17; - static constexpr uint16_t ESTIMATED_SIZE = 56; + static constexpr uint16_t ESTIMATED_SIZE = 60; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_switch_response"; } #endif @@ -880,7 +913,7 @@ class SwitchCommandRequest : public ProtoMessage { class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 18; - static constexpr uint16_t ESTIMATED_SIZE = 54; + static constexpr uint16_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_text_sensor_response"; } #endif @@ -1196,7 +1229,7 @@ class ExecuteServiceRequest : public ProtoMessage { class ListEntitiesCameraResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 43; - static constexpr uint16_t ESTIMATED_SIZE = 45; + static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_camera_response"; } #endif @@ -1253,7 +1286,7 @@ class CameraImageRequest : public ProtoMessage { class ListEntitiesClimateResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 46; - static constexpr uint16_t ESTIMATED_SIZE = 151; + static constexpr uint16_t ESTIMATED_SIZE = 156; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_climate_response"; } #endif @@ -1362,7 +1395,7 @@ class ClimateCommandRequest : public ProtoMessage { class ListEntitiesNumberResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 49; - static constexpr uint16_t ESTIMATED_SIZE = 80; + static constexpr uint16_t ESTIMATED_SIZE = 84; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_number_response"; } #endif @@ -1423,7 +1456,7 @@ class NumberCommandRequest : public ProtoMessage { class ListEntitiesSelectResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 52; - static constexpr uint16_t ESTIMATED_SIZE = 63; + static constexpr uint16_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_select_response"; } #endif @@ -1481,7 +1514,7 @@ class SelectCommandRequest : public ProtoMessage { class ListEntitiesSirenResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 55; - static constexpr uint16_t ESTIMATED_SIZE = 67; + static constexpr uint16_t ESTIMATED_SIZE = 71; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_siren_response"; } #endif @@ -1547,7 +1580,7 @@ class SirenCommandRequest : public ProtoMessage { class ListEntitiesLockResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 58; - static constexpr uint16_t ESTIMATED_SIZE = 60; + static constexpr uint16_t ESTIMATED_SIZE = 64; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_lock_response"; } #endif @@ -1609,7 +1642,7 @@ class LockCommandRequest : public ProtoMessage { class ListEntitiesButtonResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 61; - static constexpr uint16_t ESTIMATED_SIZE = 54; + static constexpr uint16_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_button_response"; } #endif @@ -1662,7 +1695,7 @@ class MediaPlayerSupportedFormat : public ProtoMessage { class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 63; - static constexpr uint16_t ESTIMATED_SIZE = 81; + static constexpr uint16_t ESTIMATED_SIZE = 85; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_media_player_response"; } #endif @@ -2532,7 +2565,7 @@ class VoiceAssistantSetConfiguration : public ProtoMessage { class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 94; - static constexpr uint16_t ESTIMATED_SIZE = 53; + static constexpr uint16_t ESTIMATED_SIZE = 57; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_alarm_control_panel_response"; } #endif @@ -2592,7 +2625,7 @@ class AlarmControlPanelCommandRequest : public ProtoMessage { class ListEntitiesTextResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 97; - static constexpr uint16_t ESTIMATED_SIZE = 64; + static constexpr uint16_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_text_response"; } #endif @@ -2653,7 +2686,7 @@ class TextCommandRequest : public ProtoMessage { class ListEntitiesDateResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 100; - static constexpr uint16_t ESTIMATED_SIZE = 45; + static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_date_response"; } #endif @@ -2713,7 +2746,7 @@ class DateCommandRequest : public ProtoMessage { class ListEntitiesTimeResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 103; - static constexpr uint16_t ESTIMATED_SIZE = 45; + static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_time_response"; } #endif @@ -2773,7 +2806,7 @@ class TimeCommandRequest : public ProtoMessage { class ListEntitiesEventResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 107; - static constexpr uint16_t ESTIMATED_SIZE = 72; + static constexpr uint16_t ESTIMATED_SIZE = 76; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_event_response"; } #endif @@ -2811,7 +2844,7 @@ class EventResponse : public StateResponseProtoMessage { class ListEntitiesValveResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 109; - static constexpr uint16_t ESTIMATED_SIZE = 60; + static constexpr uint16_t ESTIMATED_SIZE = 64; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_valve_response"; } #endif @@ -2873,7 +2906,7 @@ class ValveCommandRequest : public ProtoMessage { class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 112; - static constexpr uint16_t ESTIMATED_SIZE = 45; + static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_date_time_response"; } #endif @@ -2928,7 +2961,7 @@ class DateTimeCommandRequest : public ProtoMessage { class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 116; - static constexpr uint16_t ESTIMATED_SIZE = 54; + static constexpr uint16_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_update_response"; } #endif diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index bc26c09622..fd9551b850 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -60,8 +60,8 @@ from esphome.const import ( DEVICE_CLASS_WINDOW, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity from esphome.util import Registry CODEOWNERS = ["@esphome/core"] @@ -491,6 +491,9 @@ _BINARY_SENSOR_SCHEMA = ( ) +_BINARY_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("binary_sensor")) + + def binary_sensor_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -521,7 +524,7 @@ BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor")) async def setup_binary_sensor_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "binary_sensor") if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: cg.add(var.set_device_class(device_class)) diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 892bf62f3a..ed2670a5c5 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -18,8 +18,8 @@ from esphome.const import ( DEVICE_CLASS_UPDATE, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True @@ -61,6 +61,9 @@ _BUTTON_SCHEMA = ( ) +_BUTTON_SCHEMA.add_extra(entity_duplicate_validator("button")) + + def button_schema( class_: MockObjClass, *, @@ -87,7 +90,7 @@ BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button")) async def setup_button_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "button") for conf in config.get(CONF_ON_PRESS, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 52938a17d0..9530ecdcca 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -48,8 +48,8 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity IS_PLATFORM_COMPONENT = True @@ -247,6 +247,9 @@ _CLIMATE_SCHEMA = ( ) +_CLIMATE_SCHEMA.add_extra(entity_duplicate_validator("climate")) + + def climate_schema( class_: MockObjClass, *, @@ -273,7 +276,7 @@ CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate")) async def setup_climate_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "climate") visual = config[CONF_VISUAL] if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None: diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 9fe7593eab..cd97a38ecc 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -33,8 +33,8 @@ from esphome.const import ( DEVICE_CLASS_WINDOW, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity IS_PLATFORM_COMPONENT = True @@ -126,6 +126,9 @@ _COVER_SCHEMA = ( ) +_COVER_SCHEMA.add_extra(entity_duplicate_validator("cover")) + + def cover_schema( class_: MockObjClass, *, @@ -154,7 +157,7 @@ COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover")) async def setup_cover_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "cover") if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: cg.add(var.set_device_class(device_class)) diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 24fbf5a1ec..4788810965 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -22,8 +22,8 @@ from esphome.const import ( CONF_YEAR, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@rfdarter", "@jesserockz"] @@ -84,6 +84,8 @@ _DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) ).add_extra(_validate_time_present) +_DATETIME_SCHEMA.add_extra(entity_duplicate_validator("datetime")) + def date_schema(class_: MockObjClass) -> cv.Schema: schema = cv.Schema( @@ -133,7 +135,7 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema: async def setup_datetime_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "datetime") if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) diff --git a/esphome/components/demo/__init__.py b/esphome/components/demo/__init__.py index 0a56073284..2af0c18c18 100644 --- a/esphome/components/demo/__init__.py +++ b/esphome/components/demo/__init__.py @@ -455,7 +455,7 @@ CONFIG_SCHEMA = cv.Schema( CONF_NAME: "Demo Plain Sensor", }, { - CONF_NAME: "Demo Temperature Sensor", + CONF_NAME: "Demo Temperature Sensor 1", CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS, CONF_ICON: ICON_THERMOMETER, CONF_ACCURACY_DECIMALS: 1, @@ -463,7 +463,7 @@ CONFIG_SCHEMA = cv.Schema( CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, { - CONF_NAME: "Demo Temperature Sensor", + CONF_NAME: "Demo Temperature Sensor 2", CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS, CONF_ICON: ICON_THERMOMETER, CONF_ACCURACY_DECIMALS: 1, diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 05522265ae..cfca0ed6fc 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -19,7 +19,7 @@ from esphome.const import ( CONF_VSYNC_PIN, ) from esphome.core import CORE -from esphome.cpp_helpers import setup_entity +from esphome.core.entity_helpers import setup_entity DEPENDENCIES = ["esp32"] @@ -284,7 +284,7 @@ SETTERS = { async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await setup_entity(var, config) + await setup_entity(var, config, "camera") await cg.register_component(var, config) for key, setter in SETTERS.items(): diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index e7ab489a25..3aff96a48e 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -18,8 +18,8 @@ from esphome.const import ( DEVICE_CLASS_MOTION, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@nohat"] IS_PLATFORM_COMPONENT = True @@ -59,6 +59,9 @@ _EVENT_SCHEMA = ( ) +_EVENT_SCHEMA.add_extra(entity_duplicate_validator("event")) + + def event_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -88,7 +91,7 @@ EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event")) async def setup_event_core_(var, config, *, event_types: list[str]): - await setup_entity(var, config) + await setup_entity(var, config, "event") for conf in config.get(CONF_ON_EVENT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index c6ff938cd6..0b1d39575d 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -32,7 +32,7 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority -from esphome.cpp_helpers import setup_entity +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity IS_PLATFORM_COMPONENT = True @@ -161,6 +161,9 @@ _FAN_SCHEMA = ( ) +_FAN_SCHEMA.add_extra(entity_duplicate_validator("fan")) + + def fan_schema( class_: cg.Pvariable, *, @@ -225,7 +228,7 @@ def validate_preset_modes(value): async def setup_fan_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "fan") cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index a013029fc2..7ab899edb2 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -38,8 +38,8 @@ from esphome.const import ( CONF_WHITE, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity from .automation import LIGHT_STATE_SCHEMA from .effects import ( @@ -110,6 +110,8 @@ LIGHT_SCHEMA = ( ) ) +LIGHT_SCHEMA.add_extra(entity_duplicate_validator("light")) + BINARY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend( { cv.Optional(CONF_EFFECTS): validate_effects(BINARY_EFFECTS), @@ -207,7 +209,7 @@ def validate_color_temperature_channels(value): async def setup_light_core_(light_var, output_var, config): - await setup_entity(light_var, config) + await setup_entity(light_var, config, "light") cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE])) diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index 0fb67e3948..e62d9f3e2b 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -14,8 +14,8 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True @@ -67,6 +67,9 @@ _LOCK_SCHEMA = ( ) +_LOCK_SCHEMA.add_extra(entity_duplicate_validator("lock")) + + def lock_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -94,7 +97,7 @@ LOCK_SCHEMA.add_extra(cv.deprecated_schema_constant("lock")) async def _setup_lock_core(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "lock") for conf in config.get(CONF_ON_LOCK, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index ef76419de3..ccded1deb2 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -11,9 +11,9 @@ from esphome.const import ( CONF_VOLUME, ) from esphome.core import CORE +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.coroutine import coroutine_with_priority from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@jesserockz"] @@ -81,7 +81,7 @@ IsAnnouncingCondition = media_player_ns.class_( async def setup_media_player_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "media_player") for conf in config.get(CONF_ON_STATE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) @@ -143,6 +143,8 @@ _MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( } ) +_MEDIA_PLAYER_SCHEMA.add_extra(entity_duplicate_validator("media_player")) + def media_player_schema( class_: MockObjClass, @@ -166,7 +168,6 @@ def media_player_schema( MEDIA_PLAYER_SCHEMA = media_player_schema(MediaPlayer) MEDIA_PLAYER_SCHEMA.add_extra(cv.deprecated_schema_constant("media_player")) - MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id( cv.Schema( { diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 2567d9ffe1..4beed57188 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -76,8 +76,8 @@ from esphome.const import ( DEVICE_CLASS_WIND_SPEED, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ @@ -207,6 +207,9 @@ _NUMBER_SCHEMA = ( ) +_NUMBER_SCHEMA.add_extra(entity_duplicate_validator("number")) + + def number_schema( class_: MockObjClass, *, @@ -237,7 +240,7 @@ NUMBER_SCHEMA.add_extra(cv.deprecated_schema_constant("number")) async def setup_number_core_( var, config, *, min_value: float, max_value: float, step: float ): - await setup_entity(var, config) + await setup_entity(var, config, "number") cg.add(var.traits.set_min_value(min_value)) cg.add(var.traits.set_max_value(max_value)) diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index e14a9351a0..ed1f6c020d 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -17,8 +17,8 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True @@ -65,6 +65,9 @@ _SELECT_SCHEMA = ( ) +_SELECT_SCHEMA.add_extra(entity_duplicate_validator("select")) + + def select_schema( class_: MockObjClass, *, @@ -89,7 +92,7 @@ SELECT_SCHEMA.add_extra(cv.deprecated_schema_constant("select")) async def setup_select_core_(var, config, *, options: list[str]): - await setup_entity(var, config) + await setup_entity(var, config, "select") cg.add(var.traits.set_options(options)) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 1ad3cfabee..ea74361d51 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -101,8 +101,8 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity from esphome.util import Registry CODEOWNERS = ["@esphome/core"] @@ -318,6 +318,8 @@ _SENSOR_SCHEMA = ( ) ) +_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("sensor")) + def sensor_schema( class_: MockObjClass = cv.UNDEFINED, @@ -787,7 +789,7 @@ async def build_filters(config): async def setup_sensor_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "sensor") if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: cg.add(var.set_device_class(device_class)) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 0211c648fc..c09675069f 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -20,8 +20,8 @@ from esphome.const import ( DEVICE_CLASS_SWITCH, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True @@ -91,6 +91,9 @@ _SWITCH_SCHEMA = ( ) +_SWITCH_SCHEMA.add_extra(entity_duplicate_validator("switch")) + + def switch_schema( class_: MockObjClass, *, @@ -131,7 +134,7 @@ SWITCH_SCHEMA.add_extra(cv.deprecated_schema_constant("switch")) async def setup_switch_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "switch") if (inverted := config.get(CONF_INVERTED)) is not None: cg.add(var.set_inverted(inverted)) diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index 40b3a90d6b..8362e09ac0 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -14,8 +14,8 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@mauritskorse"] IS_PLATFORM_COMPONENT = True @@ -58,6 +58,9 @@ _TEXT_SCHEMA = ( ) +_TEXT_SCHEMA.add_extra(entity_duplicate_validator("text")) + + def text_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -94,7 +97,7 @@ async def setup_text_core_( max_length: int | None, pattern: str | None, ): - await setup_entity(var, config) + await setup_entity(var, config, "text") cg.add(var.traits.set_min_length(min_length)) cg.add(var.traits.set_max_length(max_length)) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index c7ac17c35a..abb2dcae6c 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -21,8 +21,8 @@ from esphome.const import ( DEVICE_CLASS_TIMESTAMP, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity from esphome.util import Registry DEVICE_CLASSES = [ @@ -153,6 +153,9 @@ _TEXT_SENSOR_SCHEMA = ( ) +_TEXT_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("text_sensor")) + + def text_sensor_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -186,7 +189,7 @@ async def build_filters(config): async def setup_text_sensor_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "text_sensor") if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: cg.add(var.set_device_class(device_class)) diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index 09b0698903..758267f412 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -15,8 +15,8 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@jesserockz"] IS_PLATFORM_COMPONENT = True @@ -58,6 +58,9 @@ _UPDATE_SCHEMA = ( ) +_UPDATE_SCHEMA.add_extra(entity_duplicate_validator("update")) + + def update_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -87,7 +90,7 @@ UPDATE_SCHEMA.add_extra(cv.deprecated_schema_constant("update")) async def setup_update_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "update") if device_class_config := config.get(CONF_DEVICE_CLASS): cg.add(var.set_device_class(device_class_config)) diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index a6f1428cd2..cb27546120 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -22,8 +22,8 @@ from esphome.const import ( DEVICE_CLASS_WATER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity IS_PLATFORM_COMPONENT = True @@ -103,6 +103,9 @@ _VALVE_SCHEMA = ( ) +_VALVE_SCHEMA.add_extra(entity_duplicate_validator("valve")) + + def valve_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -132,7 +135,7 @@ VALVE_SCHEMA.add_extra(cv.deprecated_schema_constant("valve")) async def _setup_valve_core(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "valve") if device_class_config := config.get(CONF_DEVICE_CLASS): cg.add(var.set_device_class(device_class_config)) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index bf69b81bb5..09b132a458 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1,5 +1,7 @@ """Helpers for config validation using voluptuous.""" +from __future__ import annotations + from contextlib import contextmanager from dataclasses import dataclass from datetime import datetime @@ -29,6 +31,7 @@ from esphome.const import ( CONF_COMMAND_RETAIN, CONF_COMMAND_TOPIC, CONF_DAY, + CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, CONF_DISCOVERY, CONF_ENTITY_CATEGORY, @@ -355,6 +358,13 @@ def icon(value): ) +def sub_device_id(value: str | None) -> core.ID: + # Lazy import to avoid circular imports + from esphome.core.config import Device + + return use_id(Device)(value) + + def boolean(value): """Validate the given config option to be a boolean. @@ -1896,6 +1906,7 @@ ENTITY_BASE_SCHEMA = Schema( Optional(CONF_DISABLED_BY_DEFAULT, default=False): boolean, Optional(CONF_ICON): icon, Optional(CONF_ENTITY_CATEGORY): entity_category, + Optional(CONF_DEVICE_ID): sub_device_id, } ) @@ -1964,7 +1975,7 @@ class Version: return f"{self.major}.{self.minor}.{self.patch}" @classmethod - def parse(cls, value: str) -> "Version": + def parse(cls, value: str) -> Version: match = re.match(r"^(\d+).(\d+).(\d+)-?\w*$", value) if match is None: raise ValueError(f"Not a valid version number {value}") diff --git a/esphome/const.py b/esphome/const.py index 5e551efc2c..ed6390d8c3 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -56,6 +56,8 @@ CONF_AP = "ap" CONF_APPARENT_POWER = "apparent_power" CONF_ARDUINO_VERSION = "arduino_version" CONF_AREA = "area" +CONF_AREA_ID = "area_id" +CONF_AREAS = "areas" CONF_ARGS = "args" CONF_ASSUMED_STATE = "assumed_state" CONF_AT = "at" @@ -217,6 +219,7 @@ CONF_DEST = "dest" CONF_DEVICE = "device" CONF_DEVICE_CLASS = "device_class" CONF_DEVICE_FACTOR = "device_factor" +CONF_DEVICE_ID = "device_id" CONF_DEVICES = "devices" CONF_DIELECTRIC_CONSTANT = "dielectric_constant" CONF_DIMENSIONS = "dimensions" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index bc98ff54db..368e2affe9 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -522,6 +522,9 @@ class EsphomeCore: # Dict to track platform entity counts for pre-allocation # Key: platform name (e.g. "sensor", "binary_sensor"), Value: count self.platform_counts: defaultdict[str, int] = defaultdict(int) + # Track entity unique IDs to handle duplicates + # Set of (device_id, platform, sanitized_name) tuples + self.unique_ids: set[tuple[str, str, str]] = set() # Whether ESPHome was started in verbose mode self.verbose = False # Whether ESPHome was started in quiet mode @@ -553,6 +556,7 @@ class EsphomeCore: self.loaded_integrations = set() self.component_ids = set() self.platform_counts = defaultdict(int) + self.unique_ids = set() PIN_SCHEMA_REGISTRY.reset() @property diff --git a/esphome/core/application.h b/esphome/core/application.h index 93d5a78958..17270ca459 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -9,6 +9,13 @@ #include "esphome/core/preferences.h" #include "esphome/core/scheduler.h" +#ifdef USE_DEVICES +#include "esphome/core/device.h" +#endif +#ifdef USE_AREAS +#include "esphome/core/area.h" +#endif + #ifdef USE_SOCKET_SELECT_SUPPORT #include #endif @@ -87,7 +94,7 @@ static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick class Application { public: - void pre_setup(const std::string &name, const std::string &friendly_name, const char *area, const char *comment, + void pre_setup(const std::string &name, const std::string &friendly_name, const char *comment, const char *compilation_time, bool name_add_mac_suffix) { arch_init(); this->name_add_mac_suffix_ = name_add_mac_suffix; @@ -102,11 +109,17 @@ class Application { this->name_ = name; this->friendly_name_ = friendly_name; } - this->area_ = area; this->comment_ = comment; this->compilation_time_ = compilation_time; } +#ifdef USE_DEVICES + void register_device(Device *device) { this->devices_.push_back(device); } +#endif +#ifdef USE_AREAS + void register_area(Area *area) { this->areas_.push_back(area); } +#endif + void set_current_component(Component *component) { this->current_component_ = component; } Component *get_current_component() { return this->current_component_; } @@ -264,6 +277,12 @@ class Application { #ifdef USE_UPDATE void reserve_update(size_t count) { this->updates_.reserve(count); } #endif +#ifdef USE_AREAS + void reserve_area(size_t count) { this->areas_.reserve(count); } +#endif +#ifdef USE_DEVICES + void reserve_device(size_t count) { this->devices_.reserve(count); } +#endif /// Register the component in this Application instance. template C *register_component(C *c) { @@ -285,7 +304,15 @@ class Application { const std::string &get_friendly_name() const { return this->friendly_name_; } /// Get the area of this Application set by pre_setup(). - std::string get_area() const { return this->area_ == nullptr ? "" : this->area_; } + const char *get_area() const { +#ifdef USE_AREAS + // If we have areas registered, return the name of the first one (which is the top-level area) + if (!this->areas_.empty() && this->areas_[0] != nullptr) { + return this->areas_[0]->get_name(); + } +#endif + return ""; + } /// Get the comment of this Application set by pre_setup(). std::string get_comment() const { return this->comment_; } @@ -334,6 +361,12 @@ class Application { uint8_t get_app_state() const { return this->app_state_; } +#ifdef USE_DEVICES + const std::vector &get_devices() { return this->devices_; } +#endif +#ifdef USE_AREAS + const std::vector &get_areas() { return this->areas_; } +#endif #ifdef USE_BINARY_SENSOR const std::vector &get_binary_sensors() { return this->binary_sensors_; } binary_sensor::BinarySensor *get_binary_sensor_by_key(uint32_t key, bool include_internal = false) { @@ -610,6 +643,12 @@ class Application { uint16_t current_loop_index_{0}; bool in_loop_{false}; +#ifdef USE_DEVICES + std::vector devices_{}; +#endif +#ifdef USE_AREAS + std::vector areas_{}; +#endif #ifdef USE_BINARY_SENSOR std::vector binary_sensors_{}; #endif @@ -676,7 +715,6 @@ class Application { std::string name_; std::string friendly_name_; - const char *area_{nullptr}; const char *comment_{nullptr}; const char *compilation_time_{nullptr}; bool name_add_mac_suffix_; diff --git a/esphome/core/area.h b/esphome/core/area.h new file mode 100644 index 0000000000..f6d88fe703 --- /dev/null +++ b/esphome/core/area.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace esphome { + +class Area { + public: + void set_area_id(uint32_t area_id) { this->area_id_ = area_id; } + uint32_t get_area_id() { return this->area_id_; } + void set_name(const char *name) { this->name_ = name; } + const char *get_name() { return this->name_; } + + protected: + uint32_t area_id_{}; + const char *name_ = ""; +}; + +} // namespace esphome diff --git a/esphome/core/config.py b/esphome/core/config.py index c407e1c11a..641c73a292 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -1,18 +1,24 @@ +from __future__ import annotations + import logging import os from pathlib import Path -from esphome import automation +from esphome import automation, core import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( CONF_AREA, + CONF_AREA_ID, + CONF_AREAS, CONF_BUILD_PATH, CONF_COMMENT, CONF_COMPILE_PROCESS_LIMIT, CONF_DEBUG_SCHEDULER, + CONF_DEVICES, CONF_ESPHOME, CONF_FRIENDLY_NAME, + CONF_ID, CONF_INCLUDES, CONF_LIBRARIES, CONF_MIN_VERSION, @@ -32,7 +38,13 @@ from esphome.const import ( __version__ as ESPHOME_VERSION, ) from esphome.core import CORE, coroutine_with_priority -from esphome.helpers import copy_file_if_changed, get_str_env, walk_files +from esphome.helpers import ( + copy_file_if_changed, + fnv1a_32bit_hash, + get_str_env, + walk_files, +) +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -48,7 +60,8 @@ LoopTrigger = cg.esphome_ns.class_( ProjectUpdateTrigger = cg.esphome_ns.class_( "ProjectUpdateTrigger", cg.Component, automation.Trigger.template(cg.std_string) ) - +Device = cg.esphome_ns.class_("Device") +Area = cg.esphome_ns.class_("Area") VALID_INCLUDE_EXTS = {".h", ".hpp", ".tcc", ".ino", ".cpp", ".c"} @@ -71,6 +84,56 @@ def validate_hostname(config): return config +def validate_ids_and_references(config: ConfigType) -> ConfigType: + """Validate that there are no hash collisions between IDs and that area_id references are valid. + + This validation is critical because we use 32-bit hashes for performance on microcontrollers. + By detecting collisions at compile time, we prevent any runtime issues while maintaining + optimal performance on 32-bit platforms. In practice, with typical deployments having only + a handful of areas and devices, hash collisions are virtually impossible. + """ + + # Helper to check hash collisions + def check_hash_collision( + id_obj: core.ID, + hash_dict: dict[int, str], + item_type: str, + path: list[str | int], + ) -> None: + hash_val: int = fnv1a_32bit_hash(id_obj.id) + if hash_val in hash_dict and hash_dict[hash_val] != id_obj.id: + raise cv.Invalid( + f"{item_type} ID '{id_obj.id}' with hash {hash_val} collides with " + f"existing {item_type.lower()} ID '{hash_dict[hash_val]}'", + path=path, + ) + hash_dict[hash_val] = id_obj.id + + # Collect all areas + all_areas: list[dict[str, str | core.ID]] = [] + if CONF_AREA in config: + all_areas.append(config[CONF_AREA]) + all_areas.extend(config[CONF_AREAS]) + + # Validate area hash collisions and collect IDs + area_hashes: dict[int, str] = {} + area_ids: set[str] = set() + for area in all_areas: + area_id: core.ID = area[CONF_ID] + check_hash_collision(area_id, area_hashes, "Area", [CONF_AREAS, area_id.id]) + area_ids.add(area_id.id) + + # Validate device hash collisions and area references + device_hashes: dict[int, str] = {} + for device in config[CONF_DEVICES]: + device_id: core.ID = device[CONF_ID] + check_hash_collision( + device_id, device_hashes, "Device", [CONF_DEVICES, device_id.id] + ) + + return config + + def valid_include(value): # Look for "<...>" includes if value.startswith("<") and value.endswith(">"): @@ -111,13 +174,32 @@ if "ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT" in os.environ: else: _compile_process_limit_default = cv.UNDEFINED +AREA_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(Area), + cv.Required(CONF_NAME): cv.string, + } +) + +DEVICE_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(Device), + cv.Required(CONF_NAME): cv.string, + cv.Optional(CONF_AREA_ID): cv.use_id(Area), + } +) + + +def validate_area_config(config: dict | str) -> dict[str, str | core.ID]: + return cv.maybe_simple_value(AREA_SCHEMA, key=CONF_NAME)(config) + CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, cv.Optional(CONF_FRIENDLY_NAME, ""): cv.string, - cv.Optional(CONF_AREA, ""): cv.string, + cv.Optional(CONF_AREA): validate_area_config, cv.Optional(CONF_COMMENT): cv.string, cv.Required(CONF_BUILD_PATH): cv.string, cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema( @@ -167,11 +249,17 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_COMPILE_PROCESS_LIMIT, default=_compile_process_limit_default ): cv.int_range(min=1, max=get_usable_cpu_count()), + cv.Optional(CONF_AREAS, default=[]): cv.ensure_list(AREA_SCHEMA), + cv.Optional(CONF_DEVICES, default=[]): cv.ensure_list(DEVICE_SCHEMA), } ), validate_hostname, ) + +FINAL_VALIDATE_SCHEMA = cv.All(validate_ids_and_references) + + PRELOAD_CONFIG_SCHEMA = cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, @@ -336,7 +424,7 @@ async def _add_platform_reserves() -> None: @coroutine_with_priority(100.0) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_global(cg.global_ns.namespace("esphome").using) # These can be used by user lambdas, put them to default scope cg.add_global(cg.RawExpression("using std::isnan")) @@ -347,7 +435,6 @@ async def to_code(config): cg.App.pre_setup( config[CONF_NAME], config[CONF_FRIENDLY_NAME], - config[CONF_AREA], config.get(CONF_COMMENT, ""), cg.RawExpression('__DATE__ ", " __TIME__'), config[CONF_NAME_ADD_MAC_SUFFIX], @@ -417,3 +504,50 @@ async def to_code(config): if config[CONF_PLATFORMIO_OPTIONS]: CORE.add_job(_add_platformio_options, config[CONF_PLATFORMIO_OPTIONS]) + + # Process areas + all_areas: list[dict[str, str | core.ID]] = [] + if CONF_AREA in config: + all_areas.append(config[CONF_AREA]) + all_areas.extend(config[CONF_AREAS]) + + if all_areas: + cg.add(cg.RawStatement(f"App.reserve_area({len(all_areas)});")) + cg.add_define("USE_AREAS") + + for area_conf in all_areas: + area_id: core.ID = area_conf[CONF_ID] + area_id_hash: int = fnv1a_32bit_hash(area_id.id) + area_name: str = area_conf[CONF_NAME] + + area_var = cg.new_Pvariable(area_id) + cg.add(area_var.set_area_id(area_id_hash)) + cg.add(area_var.set_name(area_name)) + cg.add(cg.App.register_area(area_var)) + + # Process devices + devices: list[dict[str, str | core.ID]] = config[CONF_DEVICES] + if not devices: + return + + # Reserve space for devices + cg.add(cg.RawStatement(f"App.reserve_device({len(devices)});")) + cg.add_define("USE_DEVICES") + + # Process each device + for dev_conf in devices: + device_id: core.ID = dev_conf[CONF_ID] + device_id_hash = fnv1a_32bit_hash(device_id.id) + device_name: str = dev_conf[CONF_NAME] + + dev = cg.new_Pvariable(device_id) + cg.add(dev.set_device_id(device_id_hash)) + cg.add(dev.set_name(device_name)) + + # Set area if specified + if CONF_AREA_ID in dev_conf: + area_id: core.ID = dev_conf[CONF_AREA_ID] + area_id_hash = fnv1a_32bit_hash(area_id.id) + cg.add(dev.set_area_id(area_id_hash)) + + cg.add(cg.App.register_device(dev)) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 657827c364..c9fea90386 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -20,6 +20,7 @@ // Feature flags #define USE_ALARM_CONTROL_PANEL +#define USE_AREAS #define USE_BINARY_SENSOR #define USE_BUTTON #define USE_CLIMATE @@ -29,6 +30,7 @@ #define USE_DATETIME_DATETIME #define USE_DATETIME_TIME #define USE_DEEP_SLEEP +#define USE_DEVICES #define USE_DISPLAY #define USE_ESP32_IMPROV_STATE_CALLBACK #define USE_EVENT diff --git a/esphome/core/device.h b/esphome/core/device.h new file mode 100644 index 0000000000..3d0d1e7c23 --- /dev/null +++ b/esphome/core/device.h @@ -0,0 +1,20 @@ +#pragma once + +namespace esphome { + +class Device { + public: + void set_device_id(uint32_t device_id) { this->device_id_ = device_id; } + uint32_t get_device_id() { return this->device_id_; } + void set_name(const char *name) { this->name_ = name; } + const char *get_name() { return this->name_; } + void set_area_id(uint32_t area_id) { this->area_id_ = area_id; } + uint32_t get_area_id() { return this->area_id_; } + + protected: + uint32_t device_id_{}; + uint32_t area_id_{}; + const char *name_ = ""; +}; + +} // namespace esphome diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 791b6615a1..6afd02ff65 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -11,7 +11,14 @@ const StringRef &EntityBase::get_name() const { return this->name_; } void EntityBase::set_name(const char *name) { this->name_ = StringRef(name); if (this->name_.empty()) { - this->name_ = StringRef(App.get_friendly_name()); +#ifdef USE_DEVICES + if (this->device_ != nullptr) { + this->name_ = StringRef(this->device_->get_name()); + } else +#endif + { + this->name_ = StringRef(App.get_friendly_name()); + } this->flags_.has_own_name = false; } else { this->flags_.has_own_name = true; @@ -47,19 +54,7 @@ void EntityBase::set_object_id(const char *object_id) { } // Calculate Object ID Hash from Entity Name -void EntityBase::calc_object_id_() { - // Check if `App.get_friendly_name()` is constant or dynamic. - if (!this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled()) { - // `App.get_friendly_name()` is dynamic. - const auto object_id = str_sanitize(str_snake_case(App.get_friendly_name())); - // FNV-1 hash - this->object_id_hash_ = fnv1_hash(object_id); - } else { - // `App.get_friendly_name()` is constant. - // FNV-1 hash - this->object_id_hash_ = fnv1_hash(this->object_id_c_str_); - } -} +void EntityBase::calc_object_id_() { this->object_id_hash_ = fnv1_hash(this->get_object_id()); } uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; } diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 0f0d635962..4819b66108 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -6,6 +6,10 @@ #include "helpers.h" #include "log.h" +#ifdef USE_DEVICES +#include "device.h" +#endif + namespace esphome { enum EntityCategory : uint8_t { @@ -51,6 +55,17 @@ class EntityBase { std::string get_icon() const; void set_icon(const char *icon); +#ifdef USE_DEVICES + // Get/set this entity's device id + uint32_t get_device_id() const { + if (this->device_ == nullptr) { + return 0; // No device set, return 0 + } + return this->device_->get_device_id(); + } + void set_device(Device *device) { this->device_ = device; } +#endif + // Check if this entity has state bool has_state() const { return this->flags_.has_state; } @@ -67,6 +82,9 @@ class EntityBase { const char *object_id_c_str_{nullptr}; const char *icon_c_str_{nullptr}; uint32_t object_id_hash_{}; +#ifdef USE_DEVICES + Device *device_{}; +#endif // Bit-packed flags to save memory (1 byte instead of 5) struct EntityFlags { diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 7f6a9b48ab..c95acebbf9 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -1,5 +1,116 @@ -from esphome.const import CONF_ID +from collections.abc import Callable +import logging + +import esphome.config_validation as cv +from esphome.const import ( + CONF_DEVICE_ID, + CONF_DISABLED_BY_DEFAULT, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_INTERNAL, + CONF_NAME, +) +from esphome.core import CORE, ID +from esphome.cpp_generator import MockObj, add, get_variable import esphome.final_validate as fv +from esphome.helpers import sanitize, snake_case +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) + + +def get_base_entity_object_id( + name: str, friendly_name: str | None, device_name: str | None = None +) -> str: + """Calculate the base object ID for an entity that will be set via set_object_id(). + + This function calculates what object_id_c_str_ should be set to in C++. + + The C++ EntityBase::get_object_id() (entity_base.cpp lines 38-49) works as: + - If !has_own_name && is_name_add_mac_suffix_enabled(): + return str_sanitize(str_snake_case(App.get_friendly_name())) // Dynamic + - Else: + return object_id_c_str_ ?? "" // What we set via set_object_id() + + Since we're calculating what to pass to set_object_id(), we always need to + generate the object_id the same way, regardless of name_add_mac_suffix setting. + + Args: + name: The entity name (empty string if no name) + friendly_name: The friendly name from CORE.friendly_name + device_name: The device name if entity is on a sub-device + + Returns: + The base object ID to use for duplicate checking and to pass to set_object_id() + """ + + if name: + # Entity has its own name (has_own_name will be true) + base_str = name + elif device_name: + # Entity has empty name and is on a sub-device + # C++ EntityBase::set_name() uses device->get_name() when device is set + base_str = device_name + elif friendly_name: + # Entity has empty name (has_own_name will be false) + # C++ uses App.get_friendly_name() which returns friendly_name or device name + base_str = friendly_name + else: + # Fallback to device name + base_str = CORE.name + + return sanitize(snake_case(base_str)) + + +async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: + """Set up generic properties of an Entity. + + This function sets up the common entity properties like name, icon, + entity category, etc. + + Args: + var: The entity variable to set up + config: Configuration dictionary containing entity settings + platform: The platform name (e.g., "sensor", "binary_sensor") + """ + # Get device info + device_name: str | None = None + if CONF_DEVICE_ID in config: + device_id_obj: ID = config[CONF_DEVICE_ID] + device: MockObj = await get_variable(device_id_obj) + add(var.set_device(device)) + # Get device name for object ID calculation + device_name = device_id_obj.id + + add(var.set_name(config[CONF_NAME])) + + # Calculate base object_id using the same logic as C++ + # This must match the C++ behavior in esphome/core/entity_base.cpp + base_object_id = get_base_entity_object_id( + config[CONF_NAME], CORE.friendly_name, device_name + ) + + if not config[CONF_NAME]: + _LOGGER.debug( + "Entity has empty name, using '%s' as object_id base", base_object_id + ) + + # Set the object ID + add(var.set_object_id(base_object_id)) + _LOGGER.debug( + "Setting object_id '%s' for entity '%s' on platform '%s'", + base_object_id, + config[CONF_NAME], + platform, + ) + add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) + if CONF_INTERNAL in config: + add(var.set_internal(config[CONF_INTERNAL])) + if CONF_ICON in config: + add(var.set_icon(config[CONF_ICON])) + if CONF_ENTITY_CATEGORY in config: + add(var.set_entity_category(config[CONF_ENTITY_CATEGORY])) def inherit_property_from(property_to_inherit, parent_id_property, transform=None): @@ -54,3 +165,48 @@ def inherit_property_from(property_to_inherit, parent_id_property, transform=Non return config return inherit_property + + +def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigType]: + """Create a validator function to check for duplicate entity names. + + This validator is meant to be used with schema.add_extra() for entity base schemas. + + Args: + platform: The platform name (e.g., "sensor", "binary_sensor") + + Returns: + A validator function that checks for duplicate names + """ + + def validator(config: ConfigType) -> ConfigType: + if CONF_NAME not in config: + # No name to validate + return config + + # Get the entity name and device info + entity_name = config[CONF_NAME] + device_id = "" # Empty string for main device + + if CONF_DEVICE_ID in config: + device_id_obj = config[CONF_DEVICE_ID] + # Use the device ID string directly for uniqueness + device_id = device_id_obj.id + + # For duplicate detection, just use the sanitized name + name_key = sanitize(snake_case(entity_name)) + + # Check for duplicates + unique_key = (device_id, platform, name_key) + if unique_key in CORE.unique_ids: + device_prefix = f" on device '{device_id}'" if device_id else "" + raise cv.Invalid( + f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. " + f"Each entity on a device must have a unique name within its platform." + ) + + # Add to tracking set + CORE.unique_ids.add(unique_key) + return config + + return validator diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 9a775bad33..3f64be6154 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -1,11 +1,6 @@ import logging from esphome.const import ( - CONF_DISABLED_BY_DEFAULT, - CONF_ENTITY_CATEGORY, - CONF_ICON, - CONF_INTERNAL, - CONF_NAME, CONF_SAFE_MODE, CONF_SETUP_PRIORITY, CONF_TYPE_ID, @@ -16,7 +11,6 @@ from esphome.core import CORE, ID, coroutine from esphome.coroutine import FakeAwaitable from esphome.cpp_generator import add, get_variable from esphome.cpp_types import App -from esphome.helpers import sanitize, snake_case from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry @@ -96,22 +90,6 @@ async def register_parented(var, value): add(var.set_parent(paren)) -async def setup_entity(var, config): - """Set up generic properties of an Entity""" - add(var.set_name(config[CONF_NAME])) - if not config[CONF_NAME]: - add(var.set_object_id(sanitize(snake_case(CORE.friendly_name)))) - else: - add(var.set_object_id(sanitize(snake_case(config[CONF_NAME])))) - add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) - if CONF_INTERNAL in config: - add(var.set_internal(config[CONF_INTERNAL])) - if CONF_ICON in config: - add(var.set_icon(config[CONF_ICON])) - if CONF_ENTITY_CATEGORY in config: - add(var.set_entity_category(config[CONF_ENTITY_CATEGORY])) - - def extract_registry_entry_config( registry: Registry, full_config: ConfigType, diff --git a/esphome/dashboard/util/text.py b/esphome/dashboard/util/text.py index 08d2df6abf..2a3b9042e6 100644 --- a/esphome/dashboard/util/text.py +++ b/esphome/dashboard/util/text.py @@ -1,25 +1,9 @@ from __future__ import annotations -import unicodedata - -from esphome.const import ALLOWED_NAME_CHARS +from esphome.helpers import slugify -def strip_accents(value): - return "".join( - c - for c in unicodedata.normalize("NFD", str(value)) - if unicodedata.category(c) != "Mn" - ) - - -def friendly_name_slugify(value): - value = ( - strip_accents(value) - .lower() - .replace(" ", "-") - .replace("_", "-") - .replace("--", "-") - .strip("-") - ) - return "".join(c for c in value if c in ALLOWED_NAME_CHARS) +def friendly_name_slugify(value: str) -> str: + """Convert a friendly name to a slug with dashes instead of underscores.""" + # First use the standard slugify, then convert underscores to dashes + return slugify(value).replace("_", "-") diff --git a/esphome/helpers.py b/esphome/helpers.py index d95546ac94..bf0e3b5cf7 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -29,6 +29,53 @@ def ensure_unique_string(preferred_string, current_strings): return test_string +def fnv1a_32bit_hash(string: str) -> int: + """FNV-1a 32-bit hash function. + + Note: This uses 32-bit hash instead of 64-bit for several reasons: + 1. ESPHome targets 32-bit microcontrollers with limited RAM (often <320KB) + 2. Using 64-bit hashes would double the RAM usage for storing IDs + 3. 64-bit operations are slower on 32-bit processors + + While there's a ~50% collision probability at ~77,000 unique IDs, + ESPHome validates for collisions at compile time, preventing any + runtime issues. In practice, most ESPHome installations only have + a handful of area_ids and device_ids (typically <10 areas and <100 + devices), making collisions virtually impossible. + """ + hash_value = 2166136261 + for char in string: + hash_value ^= ord(char) + hash_value = (hash_value * 16777619) & 0xFFFFFFFF + return hash_value + + +def strip_accents(value: str) -> str: + """Remove accents from a string.""" + import unicodedata + + return "".join( + c + for c in unicodedata.normalize("NFD", str(value)) + if unicodedata.category(c) != "Mn" + ) + + +def slugify(value: str) -> str: + """Convert a string to a valid C++ identifier slug.""" + from esphome.const import ALLOWED_NAME_CHARS + + value = ( + strip_accents(value) + .lower() + .replace(" ", "_") + .replace("-", "_") + .replace("__", "_") + .strip("_") + ) + return "".join(c for c in value if c in ALLOWED_NAME_CHARS) + + def indent_all_but_first_and_last(text, padding=" "): lines = text.splitlines(True) if len(lines) <= 2: diff --git a/tests/components/ade7880/common.yaml b/tests/components/ade7880/common.yaml index 0aa388a325..48c22c8485 100644 --- a/tests/components/ade7880/common.yaml +++ b/tests/components/ade7880/common.yaml @@ -12,12 +12,12 @@ sensor: frequency: 60Hz phase_a: name: Channel A - voltage: Voltage - current: Current - active_power: Active Power - power_factor: Power Factor - forward_active_energy: Forward Active Energy - reverse_active_energy: Reverse Active Energy + voltage: Channel A Voltage + current: Channel A Current + active_power: Channel A Active Power + power_factor: Channel A Power Factor + forward_active_energy: Channel A Forward Active Energy + reverse_active_energy: Channel A Reverse Active Energy calibration: current_gain: 3116628 voltage_gain: -757178 @@ -25,12 +25,12 @@ sensor: phase_angle: 188 phase_b: name: Channel B - voltage: Voltage - current: Current - active_power: Active Power - power_factor: Power Factor - forward_active_energy: Forward Active Energy - reverse_active_energy: Reverse Active Energy + voltage: Channel B Voltage + current: Channel B Current + active_power: Channel B Active Power + power_factor: Channel B Power Factor + forward_active_energy: Channel B Forward Active Energy + reverse_active_energy: Channel B Reverse Active Energy calibration: current_gain: 3133655 voltage_gain: -755235 @@ -38,12 +38,12 @@ sensor: phase_angle: 188 phase_c: name: Channel C - voltage: Voltage - current: Current - active_power: Active Power - power_factor: Power Factor - forward_active_energy: Forward Active Energy - reverse_active_energy: Reverse Active Energy + voltage: Channel C Voltage + current: Channel C Current + active_power: Channel C Active Power + power_factor: Channel C Power Factor + forward_active_energy: Channel C Forward Active Energy + reverse_active_energy: Channel C Reverse Active Energy calibration: current_gain: 3111158 voltage_gain: -743813 @@ -51,6 +51,6 @@ sensor: phase_angle: 180 neutral: name: Neutral - current: Current + current: Neutral Current calibration: current_gain: 3189 diff --git a/tests/components/alarm_control_panel/common.yaml b/tests/components/alarm_control_panel/common.yaml index 5b8ae5a282..142bf3c7e6 100644 --- a/tests/components/alarm_control_panel/common.yaml +++ b/tests/components/alarm_control_panel/common.yaml @@ -26,7 +26,7 @@ alarm_control_panel: ESP_LOGD("TEST", "State change %s", LOG_STR_ARG(alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state()))); - platform: template id: alarmcontrolpanel2 - name: Alarm Panel + name: Alarm Panel 2 codes: - "1234" requires_code_to_arm: true diff --git a/tests/components/binary_sensor_map/common.yaml b/tests/components/binary_sensor_map/common.yaml index 8ffdd1f379..2fed5ae515 100644 --- a/tests/components/binary_sensor_map/common.yaml +++ b/tests/components/binary_sensor_map/common.yaml @@ -26,7 +26,7 @@ binary_sensor: sensor: - platform: binary_sensor_map - name: Binary Sensor Map + name: Binary Sensor Map Group type: group channels: - binary_sensor: bin1 @@ -36,7 +36,7 @@ sensor: - binary_sensor: bin3 value: 100.0 - platform: binary_sensor_map - name: Binary Sensor Map + name: Binary Sensor Map Sum type: sum channels: - binary_sensor: bin1 @@ -46,7 +46,7 @@ sensor: - binary_sensor: bin3 value: 100.0 - platform: binary_sensor_map - name: Binary Sensor Map + name: Binary Sensor Map Bayesian type: bayesian prior: 0.4 observations: diff --git a/tests/components/dallas_temp/common.yaml b/tests/components/dallas_temp/common.yaml index 2f846ca278..fb51f4818e 100644 --- a/tests/components/dallas_temp/common.yaml +++ b/tests/components/dallas_temp/common.yaml @@ -5,7 +5,7 @@ one_wire: sensor: - platform: dallas_temp address: 0x1C0000031EDD2A28 - name: Dallas Temperature + name: Dallas Temperature 1 resolution: 9 - platform: dallas_temp - name: Dallas Temperature + name: Dallas Temperature 2 diff --git a/tests/components/esphome/common.yaml b/tests/components/esphome/common.yaml index 05954e37d7..a4b309b69d 100644 --- a/tests/components/esphome/common.yaml +++ b/tests/components/esphome/common.yaml @@ -2,7 +2,9 @@ esphome: debug_scheduler: true platformio_options: board_build.flash_mode: dio - area: testing + area: + id: testing_area + name: Testing Area on_boot: logger.log: on_boot on_shutdown: @@ -17,4 +19,20 @@ esphome: version: "1.1" on_update: logger.log: on_update + areas: + - id: another_area + name: Another area + devices: + - id: other_device + name: Another device + area_id: another_area + - id: test_device + name: Test device in main area + area_id: testing_area # Reference the main area (not in areas) + - id: no_area_device + name: Device without area # This device has no area_id +binary_sensor: + - platform: template + name: Other device sensor + device_id: other_device diff --git a/tests/components/heatpumpir/common.yaml b/tests/components/heatpumpir/common.yaml index 2df195c5de..d740f31518 100644 --- a/tests/components/heatpumpir/common.yaml +++ b/tests/components/heatpumpir/common.yaml @@ -7,20 +7,20 @@ climate: protocol: mitsubishi_heavy_zm horizontal_default: left vertical_default: up - name: HeatpumpIR Climate + name: HeatpumpIR Climate Mitsubishi min_temperature: 18 max_temperature: 30 - platform: heatpumpir protocol: daikin horizontal_default: mleft vertical_default: mup - name: HeatpumpIR Climate + name: HeatpumpIR Climate Daikin min_temperature: 18 max_temperature: 30 - platform: heatpumpir protocol: panasonic_altdke horizontal_default: mright vertical_default: mdown - name: HeatpumpIR Climate + name: HeatpumpIR Climate Panasonic min_temperature: 18 max_temperature: 30 diff --git a/tests/components/light/common.yaml b/tests/components/light/common.yaml index a224dbe8bc..d4f64dcdea 100644 --- a/tests/components/light/common.yaml +++ b/tests/components/light/common.yaml @@ -114,7 +114,7 @@ light: warm_white_color_temperature: 500 mireds - platform: rgb id: test_rgb_light_initial_state - name: RGB Light + name: RGB Light Initial State red: test_ledc_1 green: test_ledc_2 blue: test_ledc_3 diff --git a/tests/components/ltr390/common.yaml b/tests/components/ltr390/common.yaml index 2eebe9d1c3..e5e331e7ba 100644 --- a/tests/components/ltr390/common.yaml +++ b/tests/components/ltr390/common.yaml @@ -6,13 +6,13 @@ i2c: sensor: - platform: ltr390 uv: - name: LTR390 UV + name: LTR390 UV 1 uv_index: - name: LTR390 UVI + name: LTR390 UVI 1 light: - name: LTR390 Light + name: LTR390 Light 1 ambient_light: - name: LTR390 ALS + name: LTR390 ALS 1 gain: X3 resolution: 18 window_correction_factor: 1.0 @@ -20,13 +20,13 @@ sensor: update_interval: 60s - platform: ltr390 uv: - name: LTR390 UV + name: LTR390 UV 2 uv_index: - name: LTR390 UVI + name: LTR390 UVI 2 light: - name: LTR390 Light + name: LTR390 Light 2 ambient_light: - name: LTR390 ALS + name: LTR390 ALS 2 gain: ambient_light: X9 uv: X3 diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index 59602414a7..a035900386 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -24,33 +24,33 @@ sensor: widget: lv_arc - platform: lvgl widget: slider_id - name: LVGL Slider + name: LVGL Slider Sensor - platform: lvgl widget: bar_id id: lvgl_bar_sensor - name: LVGL Bar + name: LVGL Bar Sensor - platform: lvgl widget: spinbox_id - name: LVGL Spinbox + name: LVGL Spinbox Sensor number: - platform: lvgl widget: slider_id - name: LVGL Slider + name: LVGL Slider Number update_on_release: true restore_value: true - platform: lvgl widget: lv_arc id: lvgl_arc_number - name: LVGL Arc + name: LVGL Arc Number - platform: lvgl widget: bar_id id: lvgl_bar_number - name: LVGL Bar + name: LVGL Bar Number - platform: lvgl widget: spinbox_id id: lvgl_spinbox_number - name: LVGL Spinbox + name: LVGL Spinbox Number light: - platform: lvgl diff --git a/tests/components/opentherm/common.yaml b/tests/components/opentherm/common.yaml index 5edacc6f17..1e58a04bf0 100644 --- a/tests/components/opentherm/common.yaml +++ b/tests/components/opentherm/common.yaml @@ -170,4 +170,4 @@ switch: otc_active: name: "Boiler Outside temperature compensation active" ch2_active: - name: "Boiler Central Heating 2 active" + name: "Boiler Central Heating 2 active status" diff --git a/tests/components/packages/test.esp32-ard.yaml b/tests/components/packages/test.esp32-ard.yaml index d35c27d997..d882116c10 100644 --- a/tests/components/packages/test.esp32-ard.yaml +++ b/tests/components/packages/test.esp32-ard.yaml @@ -5,7 +5,7 @@ packages: - !include package.yaml - github://esphome/esphome/tests/components/template/common.yaml@dev - url: https://github.com/esphome/esphome - file: tests/components/binary_sensor_map/common.yaml + file: tests/components/absolute_humidity/common.yaml ref: dev refresh: 1d diff --git a/tests/components/packages/test.esp32-idf.yaml b/tests/components/packages/test.esp32-idf.yaml index 9f1484d1fd..720a5777c2 100644 --- a/tests/components/packages/test.esp32-idf.yaml +++ b/tests/components/packages/test.esp32-idf.yaml @@ -7,7 +7,7 @@ packages: shorthand: github://esphome/esphome/tests/components/template/common.yaml@dev github: url: https://github.com/esphome/esphome - file: tests/components/binary_sensor_map/common.yaml + file: tests/components/absolute_humidity/common.yaml ref: dev refresh: 1d diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index 1fb7ef6dbe..29f48d995d 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -115,7 +115,7 @@ button: address: 0x00 command: 0x0B - platform: template - name: RC5 + name: RC5 Raw on_press: remote_transmitter.transmit_raw: code: [1000, -1000] diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 3ba4c8bd07..afd393c095 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -12,7 +12,7 @@ using namespace esphome; void setup() { - App.pre_setup("livingroom", "LivingRoom", "LivingRoomArea", "comment", __DATE__ ", " __TIME__, false); + App.pre_setup("livingroom", "LivingRoom", "comment", __DATE__ ", " __TIME__, false); auto *log = new logger::Logger(115200, 512); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 525e3541b3..8f5f77ca52 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -203,6 +203,7 @@ async def compile_esphome( loop = asyncio.get_running_loop() def _read_config_and_get_binary(): + CORE.reset() # Reset CORE state between test runs CORE.config_path = str(config_path) config = esphome.config.read_config( {"command": "compile", "config": str(config_path)} diff --git a/tests/integration/fixtures/areas_and_devices.yaml b/tests/integration/fixtures/areas_and_devices.yaml new file mode 100644 index 0000000000..4a327b73a1 --- /dev/null +++ b/tests/integration/fixtures/areas_and_devices.yaml @@ -0,0 +1,57 @@ +esphome: + name: areas-devices-test + # Define top-level area + area: + id: living_room_area + name: Living Room + # Define additional areas + areas: + - id: bedroom_area + name: Bedroom + - id: kitchen_area + name: Kitchen + # Define devices with area assignments + devices: + - id: light_controller_device + name: Light Controller + area_id: living_room_area # Uses top-level area + - id: temp_sensor_device + name: Temperature Sensor + area_id: bedroom_area + - id: motion_detector_device + name: Motion Detector + area_id: living_room_area # Reuses top-level area + - id: smart_switch_device + name: Smart Switch + area_id: kitchen_area + +host: +api: +logger: + +# Sensors assigned to different devices +sensor: + - platform: template + name: Light Controller Sensor + device_id: light_controller_device + lambda: return 1.0; + update_interval: 0.1s + + - platform: template + name: Temperature Sensor Reading + device_id: temp_sensor_device + lambda: return 2.0; + update_interval: 0.1s + + - platform: template + name: Motion Detector Status + device_id: motion_detector_device + lambda: return 3.0; + update_interval: 0.1s + + - platform: template + name: Smart Switch Power + device_id: smart_switch_device + lambda: return 4.0; + update_interval: 0.1s + diff --git a/tests/integration/fixtures/duplicate_entities_on_different_devices.yaml b/tests/integration/fixtures/duplicate_entities_on_different_devices.yaml new file mode 100644 index 0000000000..ecc502ad28 --- /dev/null +++ b/tests/integration/fixtures/duplicate_entities_on_different_devices.yaml @@ -0,0 +1,154 @@ +esphome: + name: duplicate-entities-test + # Define devices to test multi-device duplicate handling + devices: + - id: controller_1 + name: Controller 1 + - id: controller_2 + name: Controller 2 + - id: controller_3 + name: Controller 3 + +host: +api: # Port will be automatically injected +logger: + +# Test that duplicate entity names are allowed on different devices + +# Scenario 1: Same sensor name on different devices (allowed) +sensor: + - platform: template + name: Temperature + device_id: controller_1 + lambda: return 21.0; + update_interval: 0.1s + + - platform: template + name: Temperature + device_id: controller_2 + lambda: return 22.0; + update_interval: 0.1s + + - platform: template + name: Temperature + device_id: controller_3 + lambda: return 23.0; + update_interval: 0.1s + + # Main device sensor (no device_id) + - platform: template + name: Temperature + lambda: return 20.0; + update_interval: 0.1s + + # Different sensor with unique name + - platform: template + name: Humidity + lambda: return 60.0; + update_interval: 0.1s + +# Scenario 2: Same binary sensor name on different devices (allowed) +binary_sensor: + - platform: template + name: Status + device_id: controller_1 + lambda: return true; + + - platform: template + name: Status + device_id: controller_2 + lambda: return false; + + - platform: template + name: Status + lambda: return true; # Main device + + # Different platform can have same name as sensor + - platform: template + name: Temperature + lambda: return true; + +# Scenario 3: Same text sensor name on different devices +text_sensor: + - platform: template + name: Device Info + device_id: controller_1 + lambda: return {"Controller 1 Active"}; + update_interval: 0.1s + + - platform: template + name: Device Info + device_id: controller_2 + lambda: return {"Controller 2 Active"}; + update_interval: 0.1s + + - platform: template + name: Device Info + lambda: return {"Main Device Active"}; + update_interval: 0.1s + +# Scenario 4: Same switch name on different devices +switch: + - platform: template + name: Power + device_id: controller_1 + lambda: return false; + turn_on_action: [] + turn_off_action: [] + + - platform: template + name: Power + device_id: controller_2 + lambda: return true; + turn_on_action: [] + turn_off_action: [] + + - platform: template + name: Power + device_id: controller_3 + lambda: return false; + turn_on_action: [] + turn_off_action: [] + + # Unique switch on main device + - platform: template + name: Main Power + lambda: return true; + turn_on_action: [] + turn_off_action: [] + +# Scenario 5: Empty names on different devices (should use device name) +button: + - platform: template + name: "" + device_id: controller_1 + on_press: [] + + - platform: template + name: "" + device_id: controller_2 + on_press: [] + + - platform: template + name: "" + on_press: [] # Main device + +# Scenario 6: Special characters in names +number: + - platform: template + name: "Temperature Setpoint!" + device_id: controller_1 + min_value: 10.0 + max_value: 30.0 + step: 0.1 + lambda: return 21.0; + set_action: [] + + - platform: template + name: "Temperature Setpoint!" + device_id: controller_2 + min_value: 10.0 + max_value: 30.0 + step: 0.1 + lambda: return 22.0; + set_action: [] diff --git a/tests/integration/fixtures/legacy_area.yaml b/tests/integration/fixtures/legacy_area.yaml new file mode 100644 index 0000000000..4d1617c395 --- /dev/null +++ b/tests/integration/fixtures/legacy_area.yaml @@ -0,0 +1,15 @@ +esphome: + name: legacy-area-test + # Using legacy string-based area configuration + area: Master Bedroom + +host: +api: +logger: + +# Simple sensor to ensure the device compiles and runs +sensor: + - platform: template + name: Test Sensor + lambda: return 42.0; + update_interval: 1s diff --git a/tests/integration/test_areas_and_devices.py b/tests/integration/test_areas_and_devices.py new file mode 100644 index 0000000000..4ce55a30a7 --- /dev/null +++ b/tests/integration/test_areas_and_devices.py @@ -0,0 +1,121 @@ +"""Integration test for areas and devices feature.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityState +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_areas_and_devices( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test areas and devices configuration with entity mapping.""" + async with run_compiled(yaml_config), api_client_connected() as client: + # Get device info which includes areas and devices + device_info = await client.device_info() + assert device_info is not None + + # Verify areas are reported + areas = device_info.areas + assert len(areas) >= 2, f"Expected at least 2 areas, got {len(areas)}" + + # Find our specific areas + main_area = next((a for a in areas if a.name == "Living Room"), None) + bedroom_area = next((a for a in areas if a.name == "Bedroom"), None) + kitchen_area = next((a for a in areas if a.name == "Kitchen"), None) + + assert main_area is not None, "Living Room area not found" + assert bedroom_area is not None, "Bedroom area not found" + assert kitchen_area is not None, "Kitchen area not found" + + # Verify devices are reported + devices = device_info.devices + assert len(devices) >= 4, f"Expected at least 4 devices, got {len(devices)}" + + # Find our specific devices + light_controller = next( + (d for d in devices if d.name == "Light Controller"), None + ) + temp_sensor = next((d for d in devices if d.name == "Temperature Sensor"), None) + motion_detector = next( + (d for d in devices if d.name == "Motion Detector"), None + ) + smart_switch = next((d for d in devices if d.name == "Smart Switch"), None) + + assert light_controller is not None, "Light Controller device not found" + assert temp_sensor is not None, "Temperature Sensor device not found" + assert motion_detector is not None, "Motion Detector device not found" + assert smart_switch is not None, "Smart Switch device not found" + + # Verify device area assignments + assert light_controller.area_id == main_area.area_id, ( + "Light Controller should be in Living Room" + ) + assert temp_sensor.area_id == bedroom_area.area_id, ( + "Temperature Sensor should be in Bedroom" + ) + assert motion_detector.area_id == main_area.area_id, ( + "Motion Detector should be in Living Room" + ) + assert smart_switch.area_id == kitchen_area.area_id, ( + "Smart Switch should be in Kitchen" + ) + + # Verify suggested_area is set to the top-level area name + assert device_info.suggested_area == "Living Room", ( + f"Expected suggested_area to be 'Living Room', got '{device_info.suggested_area}'" + ) + + # Get entity list to verify device_id mapping + entities = await client.list_entities_services() + + # Collect sensor entities + sensor_entities = [e for e in entities[0] if hasattr(e, "device_id")] + assert len(sensor_entities) >= 4, ( + f"Expected at least 4 sensor entities, got {len(sensor_entities)}" + ) + + # Subscribe to states to get sensor values + loop = asyncio.get_running_loop() + states: dict[int, EntityState] = {} + states_future: asyncio.Future[bool] = loop.create_future() + + def on_state(state: EntityState) -> None: + states[state.key] = state + # Check if we have all expected sensor states + if len(states) >= 4 and not states_future.done(): + states_future.set_result(True) + + client.subscribe_states(on_state) + + # Wait for sensor states + try: + await asyncio.wait_for(states_future, timeout=10.0) + except asyncio.TimeoutError: + pytest.fail( + f"Did not receive all sensor states within 10 seconds. " + f"Received {len(states)} states" + ) + + # Verify we have sensor entities with proper device_id assignments + device_id_mapping = { + "Light Controller Sensor": light_controller.device_id, + "Temperature Sensor Reading": temp_sensor.device_id, + "Motion Detector Status": motion_detector.device_id, + "Smart Switch Power": smart_switch.device_id, + } + + for entity in sensor_entities: + if entity.name in device_id_mapping: + expected_device_id = device_id_mapping[entity.name] + assert entity.device_id == expected_device_id, ( + f"{entity.name} has device_id {entity.device_id}, " + f"expected {expected_device_id}" + ) diff --git a/tests/integration/test_duplicate_entities.py b/tests/integration/test_duplicate_entities.py new file mode 100644 index 0000000000..99968204d4 --- /dev/null +++ b/tests/integration/test_duplicate_entities.py @@ -0,0 +1,184 @@ +"""Integration test for duplicate entity handling with new validation.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityInfo +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_duplicate_entities_on_different_devices( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that duplicate entity names are allowed on different devices.""" + async with run_compiled(yaml_config), api_client_connected() as client: + # Get device info + device_info = await client.device_info() + assert device_info is not None + + # Get devices + devices = device_info.devices + assert len(devices) >= 3, f"Expected at least 3 devices, got {len(devices)}" + + # Find our test devices + controller_1 = next((d for d in devices if d.name == "Controller 1"), None) + controller_2 = next((d for d in devices if d.name == "Controller 2"), None) + controller_3 = next((d for d in devices if d.name == "Controller 3"), None) + + assert controller_1 is not None, "Controller 1 device not found" + assert controller_2 is not None, "Controller 2 device not found" + assert controller_3 is not None, "Controller 3 device not found" + + # Get entity list + entities = await client.list_entities_services() + all_entities: list[EntityInfo] = [] + for entity_list in entities[0]: + all_entities.append(entity_list) + + # Group entities by type for easier testing + sensors = [e for e in all_entities if e.__class__.__name__ == "SensorInfo"] + binary_sensors = [ + e for e in all_entities if e.__class__.__name__ == "BinarySensorInfo" + ] + text_sensors = [ + e for e in all_entities if e.__class__.__name__ == "TextSensorInfo" + ] + switches = [e for e in all_entities if e.__class__.__name__ == "SwitchInfo"] + buttons = [e for e in all_entities if e.__class__.__name__ == "ButtonInfo"] + numbers = [e for e in all_entities if e.__class__.__name__ == "NumberInfo"] + + # Scenario 1: Check sensors with same "Temperature" name on different devices + temp_sensors = [s for s in sensors if s.name == "Temperature"] + assert len(temp_sensors) == 4, ( + f"Expected exactly 4 temperature sensors, got {len(temp_sensors)}" + ) + + # Verify each sensor is on a different device + temp_device_ids = set() + temp_object_ids = set() + + for sensor in temp_sensors: + temp_device_ids.add(sensor.device_id) + temp_object_ids.add(sensor.object_id) + + # All should have object_id "temperature" (no suffix) + assert sensor.object_id == "temperature", ( + f"Expected object_id 'temperature', got '{sensor.object_id}'" + ) + + # Should have 4 different device IDs (including None for main device) + assert len(temp_device_ids) == 4, ( + f"Temperature sensors should be on different devices, got {temp_device_ids}" + ) + + # Scenario 2: Check binary sensors "Status" on different devices + status_binary = [b for b in binary_sensors if b.name == "Status"] + assert len(status_binary) == 3, ( + f"Expected exactly 3 status binary sensors, got {len(status_binary)}" + ) + + # All should have object_id "status" + for binary in status_binary: + assert binary.object_id == "status", ( + f"Expected object_id 'status', got '{binary.object_id}'" + ) + + # Scenario 3: Check that sensor and binary_sensor can have same name + temp_binary = [b for b in binary_sensors if b.name == "Temperature"] + assert len(temp_binary) == 1, ( + f"Expected exactly 1 temperature binary sensor, got {len(temp_binary)}" + ) + assert temp_binary[0].object_id == "temperature" + + # Scenario 4: Check text sensors "Device Info" on different devices + info_text = [t for t in text_sensors if t.name == "Device Info"] + assert len(info_text) == 3, ( + f"Expected exactly 3 device info text sensors, got {len(info_text)}" + ) + + # All should have object_id "device_info" + for text in info_text: + assert text.object_id == "device_info", ( + f"Expected object_id 'device_info', got '{text.object_id}'" + ) + + # Scenario 5: Check switches "Power" on different devices + power_switches = [s for s in switches if s.name == "Power"] + assert len(power_switches) == 3, ( + f"Expected exactly 3 power switches, got {len(power_switches)}" + ) + + # All should have object_id "power" + for switch in power_switches: + assert switch.object_id == "power", ( + f"Expected object_id 'power', got '{switch.object_id}'" + ) + + # Scenario 6: Check empty name buttons (should use device name) + empty_buttons = [b for b in buttons if b.name == ""] + assert len(empty_buttons) == 3, ( + f"Expected exactly 3 empty name buttons, got {len(empty_buttons)}" + ) + + # Group by device + c1_buttons = [b for b in empty_buttons if b.device_id == controller_1.device_id] + c2_buttons = [b for b in empty_buttons if b.device_id == controller_2.device_id] + + # For main device, device_id is 0 + main_buttons = [b for b in empty_buttons if b.device_id == 0] + + # Check object IDs for empty name entities + assert len(c1_buttons) == 1 and c1_buttons[0].object_id == "controller_1" + assert len(c2_buttons) == 1 and c2_buttons[0].object_id == "controller_2" + assert ( + len(main_buttons) == 1 + and main_buttons[0].object_id == "duplicate-entities-test" + ) + + # Scenario 7: Check special characters in number names + temp_numbers = [n for n in numbers if n.name == "Temperature Setpoint!"] + assert len(temp_numbers) == 2, ( + f"Expected exactly 2 temperature setpoint numbers, got {len(temp_numbers)}" + ) + + # Special characters should be sanitized to _ in object_id + for number in temp_numbers: + assert number.object_id == "temperature_setpoint_", ( + f"Expected object_id 'temperature_setpoint_', got '{number.object_id}'" + ) + + # Verify we can get states for all entities (ensures they're functional) + loop = asyncio.get_running_loop() + states_future: asyncio.Future[None] = loop.create_future() + state_count = 0 + expected_count = ( + len(sensors) + + len(binary_sensors) + + len(text_sensors) + + len(switches) + + len(buttons) + + len(numbers) + ) + + def on_state(state) -> None: + nonlocal state_count + state_count += 1 + if state_count >= expected_count and not states_future.done(): + states_future.set_result(None) + + client.subscribe_states(on_state) + + # Wait for all entity states + try: + await asyncio.wait_for(states_future, timeout=10.0) + except asyncio.TimeoutError: + pytest.fail( + f"Did not receive all entity states within 10 seconds. " + f"Expected {expected_count}, received {state_count}" + ) diff --git a/tests/integration/test_legacy_area.py b/tests/integration/test_legacy_area.py new file mode 100644 index 0000000000..d10a01ec6a --- /dev/null +++ b/tests/integration/test_legacy_area.py @@ -0,0 +1,41 @@ +"""Integration test for legacy string-based area configuration.""" + +from __future__ import annotations + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_legacy_area( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test legacy string-based area configuration.""" + async with run_compiled(yaml_config), api_client_connected() as client: + # Get device info which includes areas + device_info = await client.device_info() + assert device_info is not None + + # Verify the area is reported (should be converted to structured format) + areas = device_info.areas + assert len(areas) == 1, f"Expected exactly 1 area, got {len(areas)}" + + # Find the area - should be slugified from "Master Bedroom" + area = areas[0] + assert area.name == "Master Bedroom", ( + f"Expected area name 'Master Bedroom', got '{area.name}'" + ) + + # Verify area.id is set (it should be a hash) + assert area.area_id > 0, "Area ID should be a positive hash value" + + # The suggested_area field should be set for backward compatibility + assert device_info.suggested_area == "Master Bedroom", ( + f"Expected suggested_area to be 'Master Bedroom', got '{device_info.suggested_area}'" + ) + + # Verify deprecated warning would have been logged during compilation + # (We can't check logs directly in integration tests, but the code should work) diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 955869b799..aac5a642f6 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -14,6 +14,8 @@ import sys import pytest +from esphome.core import CORE + here = Path(__file__).parent # Configure location of package root @@ -21,6 +23,13 @@ package_root = here.parent.parent sys.path.insert(0, package_root.as_posix()) +@pytest.fixture(autouse=True) +def reset_core(): + """Reset CORE after each test.""" + yield + CORE.reset() + + @pytest.fixture def fixture_path() -> Path: """ diff --git a/tests/unit_tests/core/__init__.py b/tests/unit_tests/core/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit_tests/core/common.py b/tests/unit_tests/core/common.py new file mode 100644 index 0000000000..1848d5397b --- /dev/null +++ b/tests/unit_tests/core/common.py @@ -0,0 +1,33 @@ +"""Common test utilities for core unit tests.""" + +from collections.abc import Callable +from pathlib import Path +from unittest.mock import patch + +from esphome import config, yaml_util +from esphome.config import Config +from esphome.core import CORE + + +def load_config_from_yaml( + yaml_file: Callable[[str], str], yaml_content: str +) -> Config | None: + """Load configuration from YAML content.""" + yaml_path = yaml_file(yaml_content) + parsed_yaml = yaml_util.load_yaml(yaml_path) + + # Mock yaml_util.load_yaml to return our parsed content + with ( + patch.object(yaml_util, "load_yaml", return_value=parsed_yaml), + patch.object(CORE, "config_path", yaml_path), + ): + return config.read_config({}) + + +def load_config_from_fixture( + yaml_file: Callable[[str], str], fixture_name: str, fixtures_dir: Path +) -> Config | None: + """Load configuration from a fixture file.""" + fixture_path = fixtures_dir / fixture_name + yaml_content = fixture_path.read_text() + return load_config_from_yaml(yaml_file, yaml_content) diff --git a/tests/unit_tests/core/conftest.py b/tests/unit_tests/core/conftest.py new file mode 100644 index 0000000000..60d6738ce9 --- /dev/null +++ b/tests/unit_tests/core/conftest.py @@ -0,0 +1,18 @@ +"""Shared fixtures for core unit tests.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + + +@pytest.fixture +def yaml_file(tmp_path: Path) -> Callable[[str], str]: + """Create a temporary YAML file for testing.""" + + def _yaml_file(content: str) -> str: + yaml_path = tmp_path / "test.yaml" + yaml_path.write_text(content) + return str(yaml_path) + + return _yaml_file diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py new file mode 100644 index 0000000000..46e3b513d7 --- /dev/null +++ b/tests/unit_tests/core/test_config.py @@ -0,0 +1,225 @@ +"""Unit tests for core config functionality including areas and devices.""" + +from collections.abc import Callable +from pathlib import Path +from typing import Any + +import pytest + +from esphome import config_validation as cv, core +from esphome.const import CONF_AREA, CONF_AREAS, CONF_DEVICES +from esphome.core.config import Area, validate_area_config + +from .common import load_config_from_fixture + +FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "config" + + +def test_validate_area_config_with_string() -> None: + """Test that string area config is converted to structured format.""" + result = validate_area_config("Living Room") + + assert isinstance(result, dict) + assert "id" in result + assert "name" in result + assert result["name"] == "Living Room" + assert isinstance(result["id"], core.ID) + assert result["id"].is_declaration + assert not result["id"].is_manual + + +def test_validate_area_config_with_dict() -> None: + """Test that structured area config passes through unchanged.""" + area_id = cv.declare_id(Area)("test_area") + input_config: dict[str, Any] = { + "id": area_id, + "name": "Test Area", + } + + result = validate_area_config(input_config) + + assert result == input_config + assert result["id"] == area_id + assert result["name"] == "Test Area" + + +def test_device_with_valid_area_id(yaml_file: Callable[[str], str]) -> None: + """Test that device with valid area_id works correctly.""" + result = load_config_from_fixture(yaml_file, "valid_area_device.yaml", FIXTURES_DIR) + assert result is not None + + esphome_config = result["esphome"] + + # Verify areas were parsed correctly + assert CONF_AREAS in esphome_config + areas = esphome_config[CONF_AREAS] + assert len(areas) == 1 + assert areas[0]["id"].id == "bedroom_area" + assert areas[0]["name"] == "Bedroom" + + # Verify devices were parsed correctly + assert CONF_DEVICES in esphome_config + devices = esphome_config[CONF_DEVICES] + assert len(devices) == 1 + assert devices[0]["id"].id == "test_device" + assert devices[0]["name"] == "Test Device" + assert devices[0]["area_id"].id == "bedroom_area" + + +def test_multiple_areas_and_devices(yaml_file: Callable[[str], str]) -> None: + """Test multiple areas and devices configuration.""" + result = load_config_from_fixture( + yaml_file, "multiple_areas_devices.yaml", FIXTURES_DIR + ) + assert result is not None + + esphome_config = result["esphome"] + + # Verify main area + assert CONF_AREA in esphome_config + main_area = esphome_config[CONF_AREA] + assert main_area["id"].id == "main_area" + assert main_area["name"] == "Main Area" + + # Verify additional areas + assert CONF_AREAS in esphome_config + areas = esphome_config[CONF_AREAS] + assert len(areas) == 2 + area_ids = {area["id"].id for area in areas} + assert area_ids == {"area1", "area2"} + + # Verify devices + assert CONF_DEVICES in esphome_config + devices = esphome_config[CONF_DEVICES] + assert len(devices) == 3 + + # Check device-area associations + device_area_map = {dev["id"].id: dev["area_id"].id for dev in devices} + assert device_area_map == { + "device1": "main_area", + "device2": "area1", + "device3": "area2", + } + + +def test_legacy_string_area( + yaml_file: Callable[[str], str], caplog: pytest.LogCaptureFixture +) -> None: + """Test legacy string area configuration with deprecation warning.""" + result = load_config_from_fixture( + yaml_file, "legacy_string_area.yaml", FIXTURES_DIR + ) + assert result is not None + + esphome_config = result["esphome"] + + # Verify the string was converted to structured format + assert CONF_AREA in esphome_config + area = esphome_config[CONF_AREA] + assert isinstance(area, dict) + assert area["name"] == "Living Room" + assert isinstance(area["id"], core.ID) + assert area["id"].is_declaration + assert not area["id"].is_manual + + +def test_area_id_collision( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that duplicate area IDs are detected.""" + result = load_config_from_fixture(yaml_file, "area_id_collision.yaml", FIXTURES_DIR) + assert result is None + + # Check for the specific error message in stdout + captured = capsys.readouterr() + # Exact duplicates are now caught by IDPassValidationStep + assert "ID duplicate_id redefined! Check esphome->area->id." in captured.out + + +def test_device_without_area(yaml_file: Callable[[str], str]) -> None: + """Test that devices without area_id work correctly.""" + result = load_config_from_fixture( + yaml_file, "device_without_area.yaml", FIXTURES_DIR + ) + assert result is not None + + esphome_config = result["esphome"] + + # Verify device was parsed + assert CONF_DEVICES in esphome_config + devices = esphome_config[CONF_DEVICES] + assert len(devices) == 1 + + device = devices[0] + assert device["id"].id == "test_device" + assert device["name"] == "Test Device" + + # Verify no area_id is present + assert "area_id" not in device + + +def test_device_with_invalid_area_id( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that device with non-existent area_id fails validation.""" + result = load_config_from_fixture( + yaml_file, "device_invalid_area.yaml", FIXTURES_DIR + ) + assert result is None + + # Check for the specific error message in stdout + captured = capsys.readouterr() + assert ( + "Couldn't find ID 'nonexistent_area'. Please check you have defined an ID with that name in your configuration." + in captured.out + ) + + +def test_device_id_hash_collision( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that device IDs with hash collisions are detected.""" + result = load_config_from_fixture( + yaml_file, "device_id_collision.yaml", FIXTURES_DIR + ) + assert result is None + + # Check for the specific error message about hash collision + captured = capsys.readouterr() + # The error message shows the ID that collides and includes the hash value + assert ( + "Device ID 'd6ka' with hash 3082558663 collides with existing device ID 'test_2258'" + in captured.out + ) + + +def test_area_id_hash_collision( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that area IDs with hash collisions are detected.""" + result = load_config_from_fixture( + yaml_file, "area_id_hash_collision.yaml", FIXTURES_DIR + ) + assert result is None + + # Check for the specific error message about hash collision + captured = capsys.readouterr() + # The error message shows the ID that collides and includes the hash value + assert ( + "Area ID 'd6ka' with hash 3082558663 collides with existing area ID 'test_2258'" + in captured.out + ) + + +def test_device_duplicate_id( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that duplicate device IDs are detected by IDPassValidationStep.""" + result = load_config_from_fixture( + yaml_file, "device_duplicate_id.yaml", FIXTURES_DIR + ) + assert result is None + + # Check for the specific error message from IDPassValidationStep + captured = capsys.readouterr() + assert "ID duplicate_device redefined!" in captured.out diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py new file mode 100644 index 0000000000..e166eeedee --- /dev/null +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -0,0 +1,595 @@ +"""Test get_base_entity_object_id function matches C++ behavior.""" + +from collections.abc import Callable, Generator +from pathlib import Path +import re +from typing import Any + +import pytest + +from esphome.config_validation import Invalid +from esphome.const import CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, CONF_ICON, CONF_NAME +from esphome.core import CORE, ID, entity_helpers +from esphome.core.entity_helpers import get_base_entity_object_id, setup_entity +from esphome.cpp_generator import MockObj +from esphome.helpers import sanitize, snake_case + +from .common import load_config_from_fixture + +# Pre-compiled regex pattern for extracting object IDs from expressions +OBJECT_ID_PATTERN = re.compile(r'\.set_object_id\(["\'](.*?)["\']\)') + +FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers" + + +@pytest.fixture(autouse=True) +def restore_core_state() -> Generator[None, None, None]: + """Save and restore CORE state for tests.""" + original_name = CORE.name + original_friendly_name = CORE.friendly_name + yield + CORE.name = original_name + CORE.friendly_name = original_friendly_name + + +def test_with_entity_name() -> None: + """Test when entity has its own name - should use entity name.""" + # Simple name + assert get_base_entity_object_id("Temperature Sensor", None) == "temperature_sensor" + assert ( + get_base_entity_object_id("Temperature Sensor", "Device Name") + == "temperature_sensor" + ) + # Even with device name, entity name takes precedence + assert ( + get_base_entity_object_id("Temperature Sensor", "Device Name", "Sub Device") + == "temperature_sensor" + ) + + # Name with special characters + assert ( + get_base_entity_object_id("Temp!@#$%^&*()Sensor", None) + == "temp__________sensor" + ) + assert get_base_entity_object_id("Temp-Sensor_123", None) == "temp-sensor_123" + + # Already snake_case + assert get_base_entity_object_id("temperature_sensor", None) == "temperature_sensor" + + # Mixed case + assert get_base_entity_object_id("TemperatureSensor", None) == "temperaturesensor" + assert get_base_entity_object_id("TEMPERATURE SENSOR", None) == "temperature_sensor" + + +def test_empty_name_with_device_name() -> None: + """Test when entity has empty name and is on a sub-device - should use device name.""" + # C++ behavior: when has_own_name is false and device is set, uses device->get_name() + assert ( + get_base_entity_object_id("", "Friendly Device", "Sub Device 1") + == "sub_device_1" + ) + assert ( + get_base_entity_object_id("", "Kitchen Controller", "controller_1") + == "controller_1" + ) + assert get_base_entity_object_id("", None, "Test-Device_123") == "test-device_123" + + +def test_empty_name_with_friendly_name() -> None: + """Test when entity has empty name and no device - should use friendly name.""" + # C++ behavior: when has_own_name is false, uses App.get_friendly_name() + assert get_base_entity_object_id("", "Friendly Device") == "friendly_device" + assert get_base_entity_object_id("", "Kitchen Controller") == "kitchen_controller" + assert get_base_entity_object_id("", "Test-Device_123") == "test-device_123" + + # Special characters in friendly name + assert get_base_entity_object_id("", "Device!@#$%") == "device_____" + + +def test_empty_name_no_friendly_name() -> None: + """Test when entity has empty name and no friendly name - should use device name.""" + # Test with CORE.name set + CORE.name = "device-name" + assert get_base_entity_object_id("", None) == "device-name" + + CORE.name = "Test Device" + assert get_base_entity_object_id("", None) == "test_device" + + +def test_edge_cases() -> None: + """Test edge cases.""" + # Only spaces + assert get_base_entity_object_id(" ", None) == "___" + + # Unicode characters (should be replaced) + assert get_base_entity_object_id("Température", None) == "temp_rature" + assert get_base_entity_object_id("测试", None) == "__" + + # Empty string with empty friendly name (empty friendly name is treated as None) + # Falls back to CORE.name + CORE.name = "device" + assert get_base_entity_object_id("", "") == "device" + + # Very long name (should work fine) + long_name = "a" * 100 + " " + "b" * 100 + expected = "a" * 100 + "_" + "b" * 100 + assert get_base_entity_object_id(long_name, None) == expected + + +@pytest.mark.parametrize( + ("name", "expected"), + [ + ("Temperature Sensor", "temperature_sensor"), + ("Living Room Light", "living_room_light"), + ("Test-Device_123", "test-device_123"), + ("Special!@#Chars", "special___chars"), + ("UPPERCASE NAME", "uppercase_name"), + ("lowercase name", "lowercase_name"), + ("Mixed Case Name", "mixed_case_name"), + (" Spaces ", "___spaces___"), + ], +) +def test_matches_cpp_helpers(name: str, expected: str) -> None: + """Test that the logic matches using snake_case and sanitize directly.""" + # For non-empty names, verify our function produces same result as direct snake_case + sanitize + assert get_base_entity_object_id(name, None) == sanitize(snake_case(name)) + assert get_base_entity_object_id(name, None) == expected + + +def test_empty_name_fallback() -> None: + """Test empty name handling which falls back to friendly_name or CORE.name.""" + # Empty name is handled specially - it doesn't just use sanitize(snake_case("")) + # Instead it falls back to friendly_name or CORE.name + assert sanitize(snake_case("")) == "" # Direct conversion gives empty string + # But our function returns a fallback + CORE.name = "device" + assert get_base_entity_object_id("", None) == "device" # Uses device name + + +def test_name_add_mac_suffix_behavior() -> None: + """Test behavior related to name_add_mac_suffix. + + In C++, when name_add_mac_suffix is enabled and entity has no name, + get_object_id() returns str_sanitize(str_snake_case(App.get_friendly_name())) + dynamically. Our function always returns the same result since we're + calculating the base for duplicate tracking. + """ + # The function should always return the same result regardless of + # name_add_mac_suffix setting, as we're calculating the base object_id + assert get_base_entity_object_id("", "Test Device") == "test_device" + assert get_base_entity_object_id("Entity Name", "Test Device") == "entity_name" + + +def test_priority_order() -> None: + """Test the priority order: entity name > device name > friendly name > CORE.name.""" + CORE.name = "core-device" + + # 1. Entity name has highest priority + assert ( + get_base_entity_object_id("Entity Name", "Friendly Name", "Device Name") + == "entity_name" + ) + + # 2. Device name is next priority (when entity name is empty) + assert ( + get_base_entity_object_id("", "Friendly Name", "Device Name") == "device_name" + ) + + # 3. Friendly name is next (when entity and device names are empty) + assert get_base_entity_object_id("", "Friendly Name", None) == "friendly_name" + + # 4. CORE.name is last resort + assert get_base_entity_object_id("", None, None) == "core-device" + + +@pytest.mark.parametrize( + ("name", "friendly_name", "device_name", "expected"), + [ + # name, friendly_name, device_name, expected + ("Living Room Light", None, None, "living_room_light"), + ("", "Kitchen Controller", None, "kitchen_controller"), + ( + "", + "ESP32 Device", + "controller_1", + "controller_1", + ), # Device name takes precedence + ("GPIO2 Button", None, None, "gpio2_button"), + ("WiFi Signal", "My Device", None, "wifi_signal"), + ("", None, "esp32_node", "esp32_node"), + ("Front Door Sensor", "Home Assistant", "door_controller", "front_door_sensor"), + ], +) +def test_real_world_examples( + name: str, friendly_name: str | None, device_name: str | None, expected: str +) -> None: + """Test real-world entity naming scenarios.""" + result = get_base_entity_object_id(name, friendly_name, device_name) + assert result == expected + + +def test_issue_6953_scenarios() -> None: + """Test specific scenarios from issue #6953.""" + # Scenario 1: Multiple empty names on main device with name_add_mac_suffix + # The Python code calculates the base, C++ might append MAC suffix dynamically + CORE.name = "device-name" + CORE.friendly_name = "Friendly Device" + + # All empty names should resolve to same base + assert get_base_entity_object_id("", CORE.friendly_name) == "friendly_device" + assert get_base_entity_object_id("", CORE.friendly_name) == "friendly_device" + assert get_base_entity_object_id("", CORE.friendly_name) == "friendly_device" + + # Scenario 2: Empty names on sub-devices + assert ( + get_base_entity_object_id("", "Main Device", "controller_1") == "controller_1" + ) + assert ( + get_base_entity_object_id("", "Main Device", "controller_2") == "controller_2" + ) + + # Scenario 3: xyz duplicates + assert get_base_entity_object_id("xyz", None) == "xyz" + assert get_base_entity_object_id("xyz", "Device") == "xyz" + + +# Tests for setup_entity function + + +@pytest.fixture +def setup_test_environment() -> Generator[list[str], None, None]: + """Set up test environment for setup_entity tests.""" + # Set CORE state for tests + CORE.name = "test-device" + CORE.friendly_name = "Test Device" + # Store original add function + + original_add = entity_helpers.add + # Track what gets added + added_expressions: list[str] = [] + + def mock_add(expression: Any) -> Any: + added_expressions.append(str(expression)) + return original_add(expression) + + # Patch add function in entity_helpers module + entity_helpers.add = mock_add + yield added_expressions + # Clean up + entity_helpers.add = original_add + + +def extract_object_id_from_expressions(expressions: list[str]) -> str | None: + """Extract the object ID that was set from the generated expressions.""" + for expr in expressions: + # Look for set_object_id calls with regex to handle various formats + # Matches: var.set_object_id("temperature_2") or var.set_object_id('temperature_2') + if match := OBJECT_ID_PATTERN.search(expr): + return match.group(1) + return None + + +@pytest.mark.asyncio +async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> None: + """Test setup_entity with unique names.""" + + added_expressions = setup_test_environment + + # Create mock entities + var1 = MockObj("sensor1") + var2 = MockObj("sensor2") + + # Set up first entity + config1 = { + CONF_NAME: "Temperature", + CONF_DISABLED_BY_DEFAULT: False, + } + await setup_entity(var1, config1, "sensor") + + # Get object ID from first entity + object_id1 = extract_object_id_from_expressions(added_expressions) + assert object_id1 == "temperature" + + # Clear for next entity + added_expressions.clear() + + # Set up second entity with different name + config2 = { + CONF_NAME: "Humidity", + CONF_DISABLED_BY_DEFAULT: False, + } + await setup_entity(var2, config2, "sensor") + + # Get object ID from second entity + object_id2 = extract_object_id_from_expressions(added_expressions) + assert object_id2 == "humidity" + + +@pytest.mark.asyncio +async def test_setup_entity_different_platforms( + setup_test_environment: list[str], +) -> None: + """Test that same name on different platforms doesn't conflict.""" + + added_expressions = setup_test_environment + + # Create mock entities + sensor = MockObj("sensor1") + binary_sensor = MockObj("binary_sensor1") + text_sensor = MockObj("text_sensor1") + + config = { + CONF_NAME: "Status", + CONF_DISABLED_BY_DEFAULT: False, + } + + # Set up entities on different platforms + platforms = [ + (sensor, "sensor"), + (binary_sensor, "binary_sensor"), + (text_sensor, "text_sensor"), + ] + + object_ids: list[str] = [] + for var, platform in platforms: + added_expressions.clear() + await setup_entity(var, config, platform) + object_id = extract_object_id_from_expressions(added_expressions) + object_ids.append(object_id) + + # All should get base object ID without suffix + assert all(obj_id == "status" for obj_id in object_ids) + + +@pytest.fixture +def mock_get_variable() -> Generator[dict[ID, MockObj], None, None]: + """Mock get_variable to return test devices.""" + devices = {} + original_get_variable = entity_helpers.get_variable + + async def _mock_get_variable(device_id: ID) -> MockObj: + if device_id in devices: + return devices[device_id] + return await original_get_variable(device_id) + + entity_helpers.get_variable = _mock_get_variable + yield devices + # Clean up + entity_helpers.get_variable = original_get_variable + + +@pytest.mark.asyncio +async def test_setup_entity_with_devices( + setup_test_environment: list[str], mock_get_variable: dict[ID, MockObj] +) -> None: + """Test that same name on different devices doesn't conflict.""" + added_expressions = setup_test_environment + + # Create mock devices + device1_id = ID("device1", type="Device") + device2_id = ID("device2", type="Device") + device1 = MockObj("device1_obj") + device2 = MockObj("device2_obj") + + # Register devices with the mock + mock_get_variable[device1_id] = device1 + mock_get_variable[device2_id] = device2 + + # Create sensors with same name on different devices + sensor1 = MockObj("sensor1") + sensor2 = MockObj("sensor2") + + config1 = { + CONF_NAME: "Temperature", + CONF_DEVICE_ID: device1_id, + CONF_DISABLED_BY_DEFAULT: False, + } + + config2 = { + CONF_NAME: "Temperature", + CONF_DEVICE_ID: device2_id, + CONF_DISABLED_BY_DEFAULT: False, + } + + # Get object IDs + object_ids: list[str] = [] + for var, config in [(sensor1, config1), (sensor2, config2)]: + added_expressions.clear() + await setup_entity(var, config, "sensor") + object_id = extract_object_id_from_expressions(added_expressions) + object_ids.append(object_id) + + # Both should get base object ID without suffix (different devices) + assert object_ids[0] == "temperature" + assert object_ids[1] == "temperature" + + +@pytest.mark.asyncio +async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> None: + """Test setup_entity with empty entity name.""" + + added_expressions = setup_test_environment + + var = MockObj("sensor1") + + config = { + CONF_NAME: "", + CONF_DISABLED_BY_DEFAULT: False, + } + + await setup_entity(var, config, "sensor") + + object_id = extract_object_id_from_expressions(added_expressions) + # Should use friendly name + assert object_id == "test_device" + + +@pytest.mark.asyncio +async def test_setup_entity_special_characters( + setup_test_environment: list[str], +) -> None: + """Test setup_entity with names containing special characters.""" + + added_expressions = setup_test_environment + + var = MockObj("sensor1") + + config = { + CONF_NAME: "Temperature Sensor!", + CONF_DISABLED_BY_DEFAULT: False, + } + + await setup_entity(var, config, "sensor") + object_id = extract_object_id_from_expressions(added_expressions) + + # Special characters should be sanitized + assert object_id == "temperature_sensor_" + + +@pytest.mark.asyncio +async def test_setup_entity_with_icon(setup_test_environment: list[str]) -> None: + """Test setup_entity sets icon correctly.""" + + added_expressions = setup_test_environment + + var = MockObj("sensor1") + + config = { + CONF_NAME: "Temperature", + CONF_DISABLED_BY_DEFAULT: False, + CONF_ICON: "mdi:thermometer", + } + + await setup_entity(var, config, "sensor") + + # Check icon was set + assert any( + 'sensor1.set_icon("mdi:thermometer")' in expr for expr in added_expressions + ) + + +@pytest.mark.asyncio +async def test_setup_entity_disabled_by_default( + setup_test_environment: list[str], +) -> None: + """Test setup_entity sets disabled_by_default correctly.""" + + added_expressions = setup_test_environment + + var = MockObj("sensor1") + + config = { + CONF_NAME: "Temperature", + CONF_DISABLED_BY_DEFAULT: True, + } + + await setup_entity(var, config, "sensor") + + # Check disabled_by_default was set + assert any( + "sensor1.set_disabled_by_default(true)" in expr for expr in added_expressions + ) + + +def test_entity_duplicate_validator() -> None: + """Test the entity_duplicate_validator function.""" + from esphome.core.entity_helpers import entity_duplicate_validator + + # Reset CORE unique_ids for clean test + CORE.unique_ids.clear() + + # Create validator for sensor platform + validator = entity_duplicate_validator("sensor") + + # First entity should pass + config1 = {CONF_NAME: "Temperature"} + validated1 = validator(config1) + assert validated1 == config1 + assert ("", "sensor", "temperature") in CORE.unique_ids + + # Second entity with different name should pass + config2 = {CONF_NAME: "Humidity"} + validated2 = validator(config2) + assert validated2 == config2 + assert ("", "sensor", "humidity") in CORE.unique_ids + + # Duplicate entity should fail + config3 = {CONF_NAME: "Temperature"} + with pytest.raises( + Invalid, match=r"Duplicate sensor entity with name 'Temperature' found" + ): + validator(config3) + + +def test_entity_duplicate_validator_with_devices() -> None: + """Test entity_duplicate_validator with devices.""" + from esphome.core.entity_helpers import entity_duplicate_validator + + # Reset CORE unique_ids for clean test + CORE.unique_ids.clear() + + # Create validator for sensor platform + validator = entity_duplicate_validator("sensor") + + # Create mock device IDs + device1 = ID("device1", type="Device") + device2 = ID("device2", type="Device") + + # Same name on different devices should pass + config1 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1} + validated1 = validator(config1) + assert validated1 == config1 + assert ("device1", "sensor", "temperature") in CORE.unique_ids + + config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device2} + validated2 = validator(config2) + assert validated2 == config2 + assert ("device2", "sensor", "temperature") in CORE.unique_ids + + # Duplicate on same device should fail + config3 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1} + with pytest.raises( + Invalid, + match=r"Duplicate sensor entity with name 'Temperature' found on device 'device1'", + ): + validator(config3) + + +def test_duplicate_entity_yaml_validation( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that duplicate entity names are caught during YAML config validation.""" + result = load_config_from_fixture(yaml_file, "duplicate_entity.yaml", FIXTURES_DIR) + assert result is None + + # Check for the duplicate entity error message + captured = capsys.readouterr() + assert "Duplicate sensor entity with name 'Temperature' found" in captured.out + + +def test_duplicate_entity_with_devices_yaml_validation( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test duplicate entity validation with devices.""" + result = load_config_from_fixture( + yaml_file, "duplicate_entity_with_devices.yaml", FIXTURES_DIR + ) + assert result is None + + # Check for the duplicate entity error message with device + captured = capsys.readouterr() + assert ( + "Duplicate sensor entity with name 'Temperature' found on device 'device1'" + in captured.out + ) + + +def test_entity_different_platforms_yaml_validation( + yaml_file: Callable[[str], str], +) -> None: + """Test that same entity name on different platforms is allowed.""" + result = load_config_from_fixture( + yaml_file, "entity_different_platforms.yaml", FIXTURES_DIR + ) + # This should succeed + assert result is not None diff --git a/tests/unit_tests/fixtures/core/config/area_id_collision.yaml b/tests/unit_tests/fixtures/core/config/area_id_collision.yaml new file mode 100644 index 0000000000..fb2e930e61 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/area_id_collision.yaml @@ -0,0 +1,10 @@ +esphome: + name: test-collision + area: + id: duplicate_id + name: Area 1 + areas: + - id: duplicate_id + name: Area 2 + +host: diff --git a/tests/unit_tests/fixtures/core/config/area_id_hash_collision.yaml b/tests/unit_tests/fixtures/core/config/area_id_hash_collision.yaml new file mode 100644 index 0000000000..3a2e8ab8a9 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/area_id_hash_collision.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + areas: + - id: test_2258 + name: "Area 1" + - id: d6ka + name: "Area 2" + +esp32: + board: esp32dev diff --git a/tests/unit_tests/fixtures/core/config/device_duplicate_id.yaml b/tests/unit_tests/fixtures/core/config/device_duplicate_id.yaml new file mode 100644 index 0000000000..2aa3055686 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/device_duplicate_id.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + devices: + - id: duplicate_device + name: "Device 1" + - id: duplicate_device + name: "Device 2" + +esp32: + board: esp32dev diff --git a/tests/unit_tests/fixtures/core/config/device_id_collision.yaml b/tests/unit_tests/fixtures/core/config/device_id_collision.yaml new file mode 100644 index 0000000000..9cf04e0595 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/device_id_collision.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + devices: + - id: test_2258 + name: "Device 1" + - id: d6ka + name: "Device 2" + +esp32: + board: esp32dev diff --git a/tests/unit_tests/fixtures/core/config/device_invalid_area.yaml b/tests/unit_tests/fixtures/core/config/device_invalid_area.yaml new file mode 100644 index 0000000000..9a8ec0a1eb --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/device_invalid_area.yaml @@ -0,0 +1,12 @@ +esphome: + name: test + areas: + - id: valid_area + name: "Valid Area" + devices: + - id: test_device + name: "Test Device" + area_id: nonexistent_area + +esp32: + board: esp32dev diff --git a/tests/unit_tests/fixtures/core/config/device_without_area.yaml b/tests/unit_tests/fixtures/core/config/device_without_area.yaml new file mode 100644 index 0000000000..8464cf37df --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/device_without_area.yaml @@ -0,0 +1,7 @@ +esphome: + name: test-device-no-area + devices: + - id: test_device + name: Test Device + +host: diff --git a/tests/unit_tests/fixtures/core/config/legacy_string_area.yaml b/tests/unit_tests/fixtures/core/config/legacy_string_area.yaml new file mode 100644 index 0000000000..fe2dc3db17 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/legacy_string_area.yaml @@ -0,0 +1,5 @@ +esphome: + name: test-legacy-area + area: Living Room + +host: diff --git a/tests/unit_tests/fixtures/core/config/multiple_areas_devices.yaml b/tests/unit_tests/fixtures/core/config/multiple_areas_devices.yaml new file mode 100644 index 0000000000..ef3b4f6e67 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/multiple_areas_devices.yaml @@ -0,0 +1,22 @@ +esphome: + name: test-multiple + area: + id: main_area + name: Main Area + areas: + - id: area1 + name: Area 1 + - id: area2 + name: Area 2 + devices: + - id: device1 + name: Device 1 + area_id: main_area + - id: device2 + name: Device 2 + area_id: area1 + - id: device3 + name: Device 3 + area_id: area2 + +host: diff --git a/tests/unit_tests/fixtures/core/config/valid_area_device.yaml b/tests/unit_tests/fixtures/core/config/valid_area_device.yaml new file mode 100644 index 0000000000..fc97894586 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/valid_area_device.yaml @@ -0,0 +1,11 @@ +esphome: + name: test-valid-area + areas: + - id: bedroom_area + name: Bedroom + devices: + - id: test_device + name: Test Device + area_id: bedroom_area + +host: diff --git a/tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity.yaml b/tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity.yaml new file mode 100644 index 0000000000..2a8dad66c9 --- /dev/null +++ b/tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity.yaml @@ -0,0 +1,13 @@ +esphome: + name: test-duplicate + +esp32: + board: esp32dev + +sensor: + - platform: template + name: "Temperature" + lambda: return 21.0; + - platform: template + name: "Temperature" # Duplicate - should fail + lambda: return 22.0; diff --git a/tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity_with_devices.yaml b/tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity_with_devices.yaml new file mode 100644 index 0000000000..42e16231a5 --- /dev/null +++ b/tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity_with_devices.yaml @@ -0,0 +1,26 @@ +esphome: + name: test-duplicate-devices + devices: + - id: device1 + name: "Device 1" + - id: device2 + name: "Device 2" + +esp32: + board: esp32dev + +sensor: + # Same name on different devices - should pass + - platform: template + device_id: device1 + name: "Temperature" + lambda: return 21.0; + - platform: template + device_id: device2 + name: "Temperature" + lambda: return 22.0; + # Duplicate on same device - should fail + - platform: template + device_id: device1 + name: "Temperature" + lambda: return 23.0; diff --git a/tests/unit_tests/fixtures/core/entity_helpers/entity_different_platforms.yaml b/tests/unit_tests/fixtures/core/entity_helpers/entity_different_platforms.yaml new file mode 100644 index 0000000000..00181c52c4 --- /dev/null +++ b/tests/unit_tests/fixtures/core/entity_helpers/entity_different_platforms.yaml @@ -0,0 +1,20 @@ +esphome: + name: test-different-platforms + +esp32: + board: esp32dev + +sensor: + - platform: template + name: "Status" + lambda: return 1.0; + +binary_sensor: + - platform: template + name: "Status" # Same name, different platform - should pass + lambda: return true; + +text_sensor: + - platform: template + name: "Status" # Same name, different platform - should pass + lambda: return {"OK"}; From f35be6b5ccae002e1b839cbc7d1bf386fe24dada Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 25 Jun 2025 22:09:43 +1000 Subject: [PATCH 115/293] [binary_sensor] Add timeout filter (#9198) --- esphome/components/binary_sensor/__init__.py | 14 +++++++++++ esphome/components/binary_sensor/filter.cpp | 6 +++++ esphome/components/binary_sensor/filter.h | 12 +++++++++- tests/components/binary_sensor/common.yaml | 25 ++++++++++++++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index fd9551b850..c97de6d5e5 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -148,6 +148,7 @@ BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Conditi # Filters Filter = binary_sensor_ns.class_("Filter") +TimeoutFilter = binary_sensor_ns.class_("TimeoutFilter", Filter, cg.Component) DelayedOnOffFilter = binary_sensor_ns.class_("DelayedOnOffFilter", Filter, cg.Component) DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter, cg.Component) DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Component) @@ -171,6 +172,19 @@ async def invert_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id) +@register_filter( + "timeout", + TimeoutFilter, + cv.templatable(cv.positive_time_period_milliseconds), +) +async def timeout_filter_to_code(config, filter_id): + var = cg.new_Pvariable(filter_id) + await cg.register_component(var, {}) + template_ = await cg.templatable(config, [], cg.uint32) + cg.add(var.set_timeout_value(template_)) + return var + + @register_filter( "delayed_on_off", DelayedOnOffFilter, diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 41d0553b35..3567e9c72b 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -25,6 +25,12 @@ void Filter::input(bool value) { } } +void TimeoutFilter::input(bool value) { + this->set_timeout("timeout", this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); }); + // we do not de-dup here otherwise changes from invalid to valid state will not be output + this->output(value); +} + optional DelayedOnOffFilter::new_value(bool value) { if (value) { this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); }); diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 65838da49d..16f44aa5fe 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -16,7 +16,7 @@ class Filter { public: virtual optional new_value(bool value) = 0; - void input(bool value); + virtual void input(bool value); void output(bool value); @@ -28,6 +28,16 @@ class Filter { Deduplicator dedup_; }; +class TimeoutFilter : public Filter, public Component { + public: + optional new_value(bool value) override { return value; } + void input(bool value) override; + template void set_timeout_value(T timeout) { this->timeout_delay_ = timeout; } + + protected: + TemplatableValue timeout_delay_{}; +}; + class DelayedOnOffFilter : public Filter, public Component { public: optional new_value(bool value) override; diff --git a/tests/components/binary_sensor/common.yaml b/tests/components/binary_sensor/common.yaml index 148b7d2405..2b4a006352 100644 --- a/tests/components/binary_sensor/common.yaml +++ b/tests/components/binary_sensor/common.yaml @@ -4,6 +4,31 @@ binary_sensor: id: some_binary_sensor name: "Random binary" lambda: return (random_uint32() & 1) == 0; + filters: + - invert: + - delayed_on: 100ms + - delayed_off: 100ms + # Templated, delays for 1s (1000ms) only if a reed switch is active + - delayed_on_off: !lambda "return 1000;" + - delayed_on_off: + time_on: 10s + time_off: !lambda "return 1000;" + - autorepeat: + - delay: 1s + time_off: 100ms + time_on: 900ms + - delay: 5s + time_off: 100ms + time_on: 400ms + - lambda: |- + if (id(some_binary_sensor).state) { + return x; + } else { + return {}; + } + - settle: 100ms + - timeout: 10s + on_state_change: then: - logger.log: From 6d0c6329ad426f2e86d8fe2e8602e255ce1a59c2 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 26 Jun 2025 08:45:14 +1000 Subject: [PATCH 116/293] [lvgl] Allow linear positioning of grid cells (#9196) --- esphome/components/lvgl/__init__.py | 2 +- esphome/components/lvgl/schemas.py | 64 +++++++++++++++++++++++-- tests/components/lvgl/lvgl-package.yaml | 4 +- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index dd49efd447..4a450375c4 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -466,7 +466,7 @@ LVGL_SCHEMA = cv.All( ): lvalid.lv_color, cv.Optional(df.CONF_THEME): cv.Schema( { - cv.Optional(name): obj_schema(w) + cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA) for name, w in WIDGET_TYPES.items() } ), diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index a0be65c928..959d203c41 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -21,7 +21,7 @@ from esphome.core.config import StartupTrigger from esphome.schema_extractors import SCHEMA_EXTRACT from . import defines as df, lv_validation as lvalid -from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR +from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR, TYPE_GRID from .helpers import add_lv_use, requires_component, validate_printf from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity from .lvcode import LvglComponent, lv_event_t_ptr @@ -349,7 +349,60 @@ def obj_schema(widget_type: WidgetType): ) +def _validate_grid_layout(config): + layout = config[df.CONF_LAYOUT] + rows = len(layout[df.CONF_GRID_ROWS]) + columns = len(layout[df.CONF_GRID_COLUMNS]) + used_cells = [[None] * columns for _ in range(rows)] + for index, widget in enumerate(config[df.CONF_WIDGETS]): + _, w = next(iter(widget.items())) + if (df.CONF_GRID_CELL_COLUMN_POS in w) != (df.CONF_GRID_CELL_ROW_POS in w): + # pylint: disable=raise-missing-from + raise cv.Invalid( + "Both row and column positions must be specified, or both omitted", + [df.CONF_WIDGETS, index], + ) + if df.CONF_GRID_CELL_ROW_POS in w: + row = w[df.CONF_GRID_CELL_ROW_POS] + column = w[df.CONF_GRID_CELL_COLUMN_POS] + else: + try: + row, column = next( + (r_idx, c_idx) + for r_idx, row in enumerate(used_cells) + for c_idx, value in enumerate(row) + if value is None + ) + except StopIteration: + # pylint: disable=raise-missing-from + raise cv.Invalid( + "No free cells available in grid layout", [df.CONF_WIDGETS, index] + ) + w[df.CONF_GRID_CELL_ROW_POS] = row + w[df.CONF_GRID_CELL_COLUMN_POS] = column + + for i in range(w[df.CONF_GRID_CELL_ROW_SPAN]): + for j in range(w[df.CONF_GRID_CELL_COLUMN_SPAN]): + if row + i >= rows or column + j >= columns: + # pylint: disable=raise-missing-from + raise cv.Invalid( + f"Cell at {row}/{column} span {w[df.CONF_GRID_CELL_ROW_SPAN]}x{w[df.CONF_GRID_CELL_COLUMN_SPAN]} " + f"exceeds grid size {rows}x{columns}", + [df.CONF_WIDGETS, index], + ) + if used_cells[row + i][column + j] is not None: + # pylint: disable=raise-missing-from + raise cv.Invalid( + f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}", + [df.CONF_WIDGETS, index], + ) + used_cells[row + i][column + j] = index + + return config + + LAYOUT_SCHEMAS = {} +LAYOUT_VALIDATORS = {TYPE_GRID: _validate_grid_layout} ALIGN_TO_SCHEMA = { cv.Optional(df.CONF_ALIGN_TO): cv.Schema( @@ -402,8 +455,8 @@ LAYOUT_SCHEMA = { } GRID_CELL_SCHEMA = { - cv.Required(df.CONF_GRID_CELL_ROW_POS): cv.positive_int, - cv.Required(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int, + cv.Optional(df.CONF_GRID_CELL_ROW_POS): cv.positive_int, + cv.Optional(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int, cv.Optional(df.CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, cv.Optional(df.CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, @@ -474,7 +527,10 @@ def container_validator(schema, widget_type: WidgetType): result = result.extend( LAYOUT_SCHEMAS.get(ltype.lower(), LAYOUT_SCHEMAS[df.TYPE_NONE]) ) - return result(value) + value = result(value) + if layout_validator := LAYOUT_VALIDATORS.get(ltype): + value = layout_validator(value) + return value return validator diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 212e30c1eb..2edc62b6a1 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -839,9 +839,7 @@ lvgl: styles: bdr_style grid_cell_x_align: center grid_cell_y_align: stretch - grid_cell_row_pos: 0 - grid_cell_column_pos: 1 - grid_cell_column_span: 1 + grid_cell_column_span: 2 text: "Grid cell 0/1" - label: grid_cell_x_align: end From 17497eec43819c989c5d77a34576d1c90a2c87cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Jun 2025 01:15:59 +0200 Subject: [PATCH 117/293] Reduce memory required for sensor entities (#9201) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/sensor/sensor.cpp | 18 ++++++++++------ esphome/components/sensor/sensor.h | 18 +++++++++++----- .../fixtures/host_mode_with_sensor.yaml | 3 +++ tests/integration/test_host_mode_sensor.py | 21 +++++++++++++++++++ 4 files changed, 49 insertions(+), 11 deletions(-) diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 6d6cff0400..7dab63b026 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -23,16 +23,22 @@ std::string state_class_to_string(StateClass state_class) { Sensor::Sensor() : state(NAN), raw_state(NAN) {} int8_t Sensor::get_accuracy_decimals() { - if (this->accuracy_decimals_.has_value()) - return *this->accuracy_decimals_; + if (this->sensor_flags_.has_accuracy_override) + return this->accuracy_decimals_; return 0; } -void Sensor::set_accuracy_decimals(int8_t accuracy_decimals) { this->accuracy_decimals_ = accuracy_decimals; } +void Sensor::set_accuracy_decimals(int8_t accuracy_decimals) { + this->accuracy_decimals_ = accuracy_decimals; + this->sensor_flags_.has_accuracy_override = true; +} -void Sensor::set_state_class(StateClass state_class) { this->state_class_ = state_class; } +void Sensor::set_state_class(StateClass state_class) { + this->state_class_ = state_class; + this->sensor_flags_.has_state_class_override = true; +} StateClass Sensor::get_state_class() { - if (this->state_class_.has_value()) - return *this->state_class_; + if (this->sensor_flags_.has_state_class_override) + return this->state_class_; return StateClass::STATE_CLASS_NONE; } diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 456e876497..3fb6e5522b 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -80,9 +80,9 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa * state changes to the database when they are published, even if the state is the * same as before. */ - bool get_force_update() const { return force_update_; } + bool get_force_update() const { return sensor_flags_.force_update; } /// Set force update mode. - void set_force_update(bool force_update) { force_update_ = force_update; } + void set_force_update(bool force_update) { sensor_flags_.force_update = force_update; } /// Add a filter to the filter chain. Will be appended to the back. void add_filter(Filter *filter); @@ -155,9 +155,17 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa Filter *filter_list_{nullptr}; ///< Store all active filters. - optional accuracy_decimals_; ///< Accuracy in decimals override - optional state_class_{STATE_CLASS_NONE}; ///< State class override - bool force_update_{false}; ///< Force update mode + // Group small members together to avoid padding + int8_t accuracy_decimals_{-1}; ///< Accuracy in decimals (-1 = not set) + StateClass state_class_{STATE_CLASS_NONE}; ///< State class (STATE_CLASS_NONE = not set) + + // Bit-packed flags for sensor-specific settings + struct SensorFlags { + uint8_t has_accuracy_override : 1; + uint8_t has_state_class_override : 1; + uint8_t force_update : 1; + uint8_t reserved : 5; // Reserved for future use + } sensor_flags_{}; }; } // namespace sensor diff --git a/tests/integration/fixtures/host_mode_with_sensor.yaml b/tests/integration/fixtures/host_mode_with_sensor.yaml index fecd0b435b..0ac495f3b1 100644 --- a/tests/integration/fixtures/host_mode_with_sensor.yaml +++ b/tests/integration/fixtures/host_mode_with_sensor.yaml @@ -8,5 +8,8 @@ sensor: name: Test Sensor id: test_sensor unit_of_measurement: °C + accuracy_decimals: 2 + state_class: measurement + force_update: true lambda: return 42.0; update_interval: 0.1s diff --git a/tests/integration/test_host_mode_sensor.py b/tests/integration/test_host_mode_sensor.py index f0c938da1c..049f7db619 100644 --- a/tests/integration/test_host_mode_sensor.py +++ b/tests/integration/test_host_mode_sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio +import aioesphomeapi from aioesphomeapi import EntityState import pytest @@ -47,3 +48,23 @@ async def test_host_mode_with_sensor( # Verify the sensor state assert test_sensor_state.state == 42.0 assert len(states) > 0, "No states received" + + # Verify the optimized fields are working correctly + # Get entity info to check accuracy_decimals, state_class, etc. + entities, _ = await client.list_entities_services() + sensor_info: aioesphomeapi.SensorInfo | None = None + for entity in entities: + if isinstance(entity, aioesphomeapi.SensorInfo): + sensor_info = entity + break + + assert sensor_info is not None, "Sensor entity info not found" + assert sensor_info.accuracy_decimals == 2, ( + f"Expected accuracy_decimals=2, got {sensor_info.accuracy_decimals}" + ) + assert sensor_info.state_class == 1, ( + f"Expected state_class=1 (measurement), got {sensor_info.state_class}" + ) + assert sensor_info.force_update is True, ( + f"Expected force_update=True, got {sensor_info.force_update}" + ) From e0172504455de27b43833c0900fc7c1d33f8f3b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Jun 2025 03:44:07 +0200 Subject: [PATCH 118/293] Reduce logger CPU usage by disabling loop when buffer is empty (#9160) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/logger/logger.cpp | 13 +++++++++++++ esphome/components/logger/logger.h | 20 ++++++++++++++++++++ esphome/core/defines.h | 2 ++ 3 files changed, 35 insertions(+) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index b42496af66..a2c2aa0320 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -48,6 +48,11 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch // For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered message_sent = this->log_buffer_->send_message_thread_safe(level, tag, static_cast(line), current_task, format, args); + if (message_sent) { + // Enable logger loop to process the buffered message + // This is safe to call from any context including ISRs + this->enable_loop_soon_any_context(); + } #endif // USE_ESPHOME_TASK_LOG_BUFFER // Emergency console logging for non-main tasks when ring buffer is full or disabled @@ -139,6 +144,10 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate #ifdef USE_ESPHOME_TASK_LOG_BUFFER void Logger::init_log_buffer(size_t total_buffer_size) { this->log_buffer_ = esphome::make_unique(total_buffer_size); + + // Start with loop disabled when using task buffer (unless using USB CDC) + // The loop will be enabled automatically when messages arrive + this->disable_loop_when_buffer_empty_(); } #endif @@ -189,6 +198,10 @@ void Logger::loop() { this->write_msg_(this->tx_buffer_); } } + } else { + // No messages to process, disable loop if appropriate + // This reduces overhead when there's no async logging activity + this->disable_loop_when_buffer_empty_(); } #endif } diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index ea82764393..38faf73d84 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -358,6 +358,26 @@ class Logger : public Component { static const uint16_t RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR); this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); } + +#ifdef USE_ESP32 + // Disable loop when task buffer is empty (with USB CDC check) + inline void disable_loop_when_buffer_empty_() { + // Thread safety note: This is safe even if another task calls enable_loop_soon_any_context() + // concurrently. If that happens between our check and disable_loop(), the enable request + // will be processed on the next main loop iteration since: + // - disable_loop() takes effect immediately + // - enable_loop_soon_any_context() sets a pending flag that's checked at loop start +#if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO) + // Only disable if not using USB CDC (which needs loop for connection detection) + if (this->uart_ != UART_SELECTION_USB_CDC) { + this->disable_loop(); + } +#else + // No USB CDC support, always safe to disable + this->disable_loop(); +#endif + } +#endif }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index c9fea90386..8abd6598f7 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -132,6 +132,8 @@ // ESP32-specific feature flags #ifdef USE_ESP32 +#define USE_ESPHOME_TASK_LOG_BUFFER + #define USE_BLUETOOTH_PROXY #define USE_CAPTIVE_PORTAL #define USE_ESP32_BLE From 15ef93ccc9e7e3a93ad77e11e14b0a16561801f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Jun 2025 03:47:41 +0200 Subject: [PATCH 119/293] Optimize API connection loop performance (#9184) --- esphome/components/api/api_connection.cpp | 54 +++++------ esphome/components/api/api_frame_helper.cpp | 46 ++++----- esphome/components/api/api_frame_helper.h | 4 +- esphome/components/api/api_server.cpp | 93 ++++++++++++------- esphome/components/api/api_server.h | 2 +- .../fixtures/api_reboot_timeout.yaml | 7 ++ tests/integration/test_api_reboot_timeout.py | 35 +++++++ 7 files changed, 146 insertions(+), 95 deletions(-) create mode 100644 tests/integration/fixtures/api_reboot_timeout.yaml create mode 100644 tests/integration/test_api_reboot_timeout.py diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 634174ce0a..e9b46853f4 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -33,9 +33,14 @@ namespace api { // Since each message could contain multiple protobuf messages when using packet batching, // this limits the number of messages processed, not the number of TCP packets. static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5; +static constexpr uint8_t MAX_PING_RETRIES = 60; +static constexpr uint16_t PING_RETRY_INTERVAL = 1000; +static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2; static const char *const TAG = "api.connection"; +#ifdef USE_ESP32_CAMERA static const int ESP32_CAMERA_STOP_STREAM = 5000; +#endif APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { @@ -90,16 +95,6 @@ APIConnection::~APIConnection() { } void APIConnection::loop() { - if (this->remove_) - return; - - if (!network::is_connected()) { - // when network is disconnected force disconnect immediately - // don't wait for timeout - this->on_fatal_error(); - ESP_LOGW(TAG, "%s: Network unavailable; disconnecting", this->get_client_combined_info().c_str()); - return; - } if (this->next_close_) { // requested a disconnect this->helper_->close(); @@ -152,20 +147,19 @@ void APIConnection::loop() { // Process deferred batch if scheduled if (this->deferred_batch_.batch_scheduled && - App.get_loop_component_start_time() - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { + now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { this->process_batch_(); } - if (!this->list_entities_iterator_.completed()) + if (!this->list_entities_iterator_.completed()) { this->list_entities_iterator_.advance(); - if (!this->initial_state_iterator_.completed() && this->list_entities_iterator_.completed()) + } else if (!this->initial_state_iterator_.completed()) { this->initial_state_iterator_.advance(); + } - static uint8_t max_ping_retries = 60; - static uint16_t ping_retry_interval = 1000; if (this->sent_ping_) { // Disconnect if not responded within 2.5*keepalive - if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) { + if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { on_fatal_error(); ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); } @@ -173,17 +167,15 @@ void APIConnection::loop() { ESP_LOGVV(TAG, "Sending keepalive PING"); this->sent_ping_ = this->send_message(PingRequest()); if (!this->sent_ping_) { - this->next_ping_retry_ = now + ping_retry_interval; + this->next_ping_retry_ = now + PING_RETRY_INTERVAL; this->ping_retries_++; - std::string warn_str = str_sprintf("%s: Sending keepalive failed %u time(s);", - this->get_client_combined_info().c_str(), this->ping_retries_); - if (this->ping_retries_ >= max_ping_retries) { + if (this->ping_retries_ >= MAX_PING_RETRIES) { on_fatal_error(); - ESP_LOGE(TAG, "%s disconnecting", warn_str.c_str()); + ESP_LOGE(TAG, "%s: Ping failed %u times", this->get_client_combined_info().c_str(), this->ping_retries_); } else if (this->ping_retries_ >= 10) { - ESP_LOGW(TAG, "%s retrying in %u ms", warn_str.c_str(), ping_retry_interval); + ESP_LOGW(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_); } else { - ESP_LOGD(TAG, "%s retrying in %u ms", warn_str.c_str(), ping_retry_interval); + ESP_LOGD(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_); } } } @@ -207,22 +199,20 @@ void APIConnection::loop() { // bool done = 3; buffer.encode_bool(3, done); - bool success = this->send_buffer(buffer, 44); + bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE); if (success) { this->image_reader_.consume_data(to_send); - } - if (success && done) { - this->image_reader_.return_image(); + if (done) { + this->image_reader_.return_image(); + } } } #endif - if (state_subs_at_ != -1) { + if (state_subs_at_ >= 0) { const auto &subs = this->parent_->get_state_subs(); - if (state_subs_at_ >= (int) subs.size()) { - state_subs_at_ = -1; - } else { + if (state_subs_at_ < static_cast(subs.size())) { auto &it = subs[state_subs_at_]; SubscribeHomeAssistantStateResponse resp; resp.entity_id = it.entity_id; @@ -231,6 +221,8 @@ void APIConnection::loop() { if (this->send_message(resp)) { state_subs_at_++; } + } else { + state_subs_at_ = -1; } } } diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index ff660f439e..af6dd0220d 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -66,6 +66,17 @@ const char *api_error_to_str(APIError err) { return "UNKNOWN"; } +// Default implementation for loop - handles sending buffered data +APIError APIFrameHelper::loop() { + if (!this->tx_buf_.empty()) { + APIError err = try_send_tx_buf_(); + if (err != APIError::OK && err != APIError::WOULD_BLOCK) { + return err; + } + } + return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination +} + // Helper method to buffer data from IOVs void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { SendBuffer buffer; @@ -287,13 +298,8 @@ APIError APINoiseFrameHelper::loop() { } } - if (!this->tx_buf_.empty()) { - APIError err = try_send_tx_buf_(); - if (err != APIError::OK && err != APIError::WOULD_BLOCK) { - return err; - } - } - return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination + // Use base class implementation for buffer sending + return APIFrameHelper::loop(); } /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter @@ -339,17 +345,15 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { return APIError::WOULD_BLOCK; } + if (rx_header_buf_[0] != 0x01) { + state_ = State::FAILED; + HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); + return APIError::BAD_INDICATOR; + } // header reading done } // read body - uint8_t indicator = rx_header_buf_[0]; - if (indicator != 0x01) { - state_ = State::FAILED; - HELPER_LOG("Bad indicator byte %u", indicator); - return APIError::BAD_INDICATOR; - } - uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; if (state_ != State::DATA && msg_size > 128) { @@ -595,10 +599,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::BAD_DATA_PACKET; } - // uint16_t type; - // uint16_t data_len; - // uint8_t *data; - // uint8_t *padding; zero or more bytes to fill up the rest of the packet uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; if (data_len > msg_size - 4) { @@ -831,18 +831,12 @@ APIError APIPlaintextFrameHelper::init() { state_ = State::DATA; return APIError::OK; } -/// Not used for plaintext APIError APIPlaintextFrameHelper::loop() { if (state_ != State::DATA) { return APIError::BAD_STATE; } - if (!this->tx_buf_.empty()) { - APIError err = try_send_tx_buf_(); - if (err != APIError::OK && err != APIError::WOULD_BLOCK) { - return err; - } - } - return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination + // Use base class implementation for buffer sending + return APIFrameHelper::loop(); } /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 7e90153091..1e157278a1 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -38,7 +38,7 @@ struct PacketInfo { : message_type(type), offset(off), payload_size(size), padding(0) {} }; -enum class APIError : int { +enum class APIError : uint16_t { OK = 0, WOULD_BLOCK = 1001, BAD_HANDSHAKE_PACKET_LEN = 1002, @@ -74,7 +74,7 @@ class APIFrameHelper { } virtual ~APIFrameHelper() = default; virtual APIError init() = 0; - virtual APIError loop() = 0; + virtual APIError loop(); virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } std::string getpeername() { return socket_->getpeername(); } diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 740e4259b1..583837af82 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -47,6 +47,11 @@ void APIServer::setup() { } #endif + // Schedule reboot if no clients connect within timeout + if (this->reboot_timeout_ != 0) { + this->schedule_reboot_timeout_(); + } + this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections if (this->socket_ == nullptr) { ESP_LOGW(TAG, "Could not create socket"); @@ -106,8 +111,6 @@ void APIServer::setup() { } #endif - this->last_connected_ = App.get_loop_component_start_time(); - #ifdef USE_ESP32_CAMERA if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) { esp32_camera::global_esp32_camera->add_image_callback( @@ -121,6 +124,16 @@ void APIServer::setup() { #endif } +void APIServer::schedule_reboot_timeout_() { + this->status_set_warning(); + this->set_timeout("api_reboot", this->reboot_timeout_, []() { + if (!global_api_server->is_connected()) { + ESP_LOGE(TAG, "No clients; rebooting"); + App.reboot(); + } + }); +} + void APIServer::loop() { // Accept new clients only if the socket exists and has incoming connections if (this->socket_ && this->socket_->ready()) { @@ -130,51 +143,61 @@ void APIServer::loop() { auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); if (!sock) break; - ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str()); + ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str()); auto *conn = new APIConnection(std::move(sock), this); this->clients_.emplace_back(conn); conn->start(); + + // Clear warning status and cancel reboot when first client connects + if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { + this->status_clear_warning(); + this->cancel_timeout("api_reboot"); + } } } + if (this->clients_.empty()) { + return; + } + // Process clients and remove disconnected ones in a single pass - if (!this->clients_.empty()) { - size_t client_index = 0; - while (client_index < this->clients_.size()) { - auto &client = this->clients_[client_index]; - - if (client->remove_) { - // Handle disconnection - this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_); - ESP_LOGV(TAG, "Removing connection to %s", client->client_info_.c_str()); - - // Swap with the last element and pop (avoids expensive vector shifts) - if (client_index < this->clients_.size() - 1) { - std::swap(this->clients_[client_index], this->clients_.back()); - } - this->clients_.pop_back(); - // Don't increment client_index since we need to process the swapped element - } else { - // Process active client - client->loop(); - client_index++; // Move to next client - } + // Check network connectivity once for all clients + if (!network::is_connected()) { + // Network is down - disconnect all clients + for (auto &client : this->clients_) { + client->on_fatal_error(); + ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str()); } + // Continue to process and clean up the clients below } - if (this->reboot_timeout_ != 0) { - const uint32_t now = App.get_loop_component_start_time(); - if (!this->is_connected()) { - if (now - this->last_connected_ > this->reboot_timeout_) { - ESP_LOGE(TAG, "No client connected; rebooting"); - App.reboot(); - } - this->status_set_warning(); - } else { - this->last_connected_ = now; - this->status_clear_warning(); + size_t client_index = 0; + while (client_index < this->clients_.size()) { + auto &client = this->clients_[client_index]; + + if (!client->remove_) { + // Common case: process active client + client->loop(); + client_index++; + continue; } + + // Rare case: handle disconnection + this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_); + ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str()); + + // Swap with the last element and pop (avoids expensive vector shifts) + if (client_index < this->clients_.size() - 1) { + std::swap(this->clients_[client_index], this->clients_.back()); + } + this->clients_.pop_back(); + + // Schedule reboot when last client disconnects + if (this->clients_.empty() && this->reboot_timeout_ != 0) { + this->schedule_reboot_timeout_(); + } + // Don't increment client_index since we need to process the swapped element } } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 33412d8a68..27341dc596 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -142,6 +142,7 @@ class APIServer : public Component, public Controller { } protected: + void schedule_reboot_timeout_(); // Pointers and pointer-like types first (4 bytes each) std::unique_ptr socket_ = nullptr; Trigger *client_connected_trigger_ = new Trigger(); @@ -150,7 +151,6 @@ class APIServer : public Component, public Controller { // 4-byte aligned types uint32_t reboot_timeout_{300000}; uint32_t batch_delay_{100}; - uint32_t last_connected_{0}; // Vectors and strings (12 bytes each on 32-bit) std::vector> clients_; diff --git a/tests/integration/fixtures/api_reboot_timeout.yaml b/tests/integration/fixtures/api_reboot_timeout.yaml new file mode 100644 index 0000000000..881bb5b2fc --- /dev/null +++ b/tests/integration/fixtures/api_reboot_timeout.yaml @@ -0,0 +1,7 @@ +esphome: + name: api-reboot-test +host: +api: + reboot_timeout: 0.5s # Very short timeout for fast testing +logger: + level: DEBUG diff --git a/tests/integration/test_api_reboot_timeout.py b/tests/integration/test_api_reboot_timeout.py new file mode 100644 index 0000000000..dd9f5fbd1e --- /dev/null +++ b/tests/integration/test_api_reboot_timeout.py @@ -0,0 +1,35 @@ +"""Test API server reboot timeout functionality.""" + +import asyncio +import re + +import pytest + +from .types import RunCompiledFunction + + +@pytest.mark.asyncio +async def test_api_reboot_timeout( + yaml_config: str, + run_compiled: RunCompiledFunction, +) -> None: + """Test that the device reboots when no API clients connect within the timeout.""" + loop = asyncio.get_running_loop() + reboot_future = loop.create_future() + reboot_pattern = re.compile(r"No clients; rebooting") + + def check_output(line: str) -> None: + """Check output for reboot message.""" + if not reboot_future.done() and reboot_pattern.search(line): + reboot_future.set_result(True) + + # Run the device without connecting any API client + async with run_compiled(yaml_config, line_callback=check_output): + # Wait for reboot with timeout + # (0.5s reboot timeout + some margin for processing) + try: + await asyncio.wait_for(reboot_future, timeout=2.0) + except asyncio.TimeoutError: + pytest.fail("Device did not reboot within expected timeout") + + # Test passes if we get here - reboot was detected From c74e5e0f04da6ccb9d9fcbd24569f403588efe45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Jun 2025 03:51:51 +0200 Subject: [PATCH 120/293] Optimize TemplatableValue memory (#9202) --- esphome/core/automation.h | 67 +++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 02c9d44f16..e156818312 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -27,20 +27,67 @@ template class TemplatableValue { public: TemplatableValue() : type_(NONE) {} - template::value, int> = 0> - TemplatableValue(F value) : type_(VALUE), value_(std::move(value)) {} + template::value, int> = 0> TemplatableValue(F value) : type_(VALUE) { + new (&this->value_) T(std::move(value)); + } - template::value, int> = 0> - TemplatableValue(F f) : type_(LAMBDA), f_(f) {} + template::value, int> = 0> TemplatableValue(F f) : type_(LAMBDA) { + this->f_ = new std::function(std::move(f)); + } + + // Copy constructor + TemplatableValue(const TemplatableValue &other) : type_(other.type_) { + if (type_ == VALUE) { + new (&this->value_) T(other.value_); + } else if (type_ == LAMBDA) { + this->f_ = new std::function(*other.f_); + } + } + + // Move constructor + TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) { + if (type_ == VALUE) { + new (&this->value_) T(std::move(other.value_)); + } else if (type_ == LAMBDA) { + this->f_ = other.f_; + other.f_ = nullptr; + } + other.type_ = NONE; + } + + // Assignment operators + TemplatableValue &operator=(const TemplatableValue &other) { + if (this != &other) { + this->~TemplatableValue(); + new (this) TemplatableValue(other); + } + return *this; + } + + TemplatableValue &operator=(TemplatableValue &&other) noexcept { + if (this != &other) { + this->~TemplatableValue(); + new (this) TemplatableValue(std::move(other)); + } + return *this; + } + + ~TemplatableValue() { + if (type_ == VALUE) { + this->value_.~T(); + } else if (type_ == LAMBDA) { + delete this->f_; + } + } bool has_value() { return this->type_ != NONE; } T value(X... x) { if (this->type_ == LAMBDA) { - return this->f_(x...); + return (*this->f_)(x...); } // return value also when none - return this->value_; + return this->type_ == VALUE ? this->value_ : T{}; } optional optional_value(X... x) { @@ -58,14 +105,16 @@ template class TemplatableValue { } protected: - enum { + enum : uint8_t { NONE, VALUE, LAMBDA, } type_; - T value_{}; - std::function f_{}; + union { + T value_; + std::function *f_; + }; }; /** Base class for all automation conditions. From 79e3d2b2d78c52e4a00f0e2a177736c1f489bf4b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Jun 2025 03:55:12 +0200 Subject: [PATCH 121/293] Optimize API connection memory with tagged pointers (#9203) --- esphome/components/api/api_connection.cpp | 35 +++++----- esphome/components/api/api_connection.h | 83 ++++++++++++----------- 2 files changed, 64 insertions(+), 54 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index e9b46853f4..b7f6f04be0 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1432,7 +1432,7 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe #ifdef USE_EVENT void APIConnection::send_event(event::Event *event, const std::string &event_type) { - this->schedule_message_(event, MessageCreator(event_type, EventResponse::MESSAGE_TYPE), EventResponse::MESSAGE_TYPE); + this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE); } void APIConnection::send_event_info(event::Event *event) { this->schedule_message_(event, &APIConnection::try_send_event_info, ListEntitiesEventResponse::MESSAGE_TYPE); @@ -1787,7 +1787,8 @@ void APIConnection::process_batch_() { const auto &item = this->deferred_batch_.items[0]; // Let the creator calculate size and encode if it fits - uint16_t payload_size = item.creator(item.entity, this, std::numeric_limits::max(), true); + uint16_t payload_size = + item.creator(item.entity, this, std::numeric_limits::max(), true, item.message_type); if (payload_size > 0 && this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) { @@ -1837,7 +1838,7 @@ void APIConnection::process_batch_() { for (const auto &item : this->deferred_batch_.items) { // Try to encode message // The creator will calculate overhead to determine if the message fits - uint16_t payload_size = item.creator(item.entity, this, remaining_size, false); + uint16_t payload_size = item.creator(item.entity, this, remaining_size, false, item.message_type); if (payload_size == 0) { // Message won't fit, stop processing @@ -1900,21 +1901,23 @@ void APIConnection::process_batch_() { } uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) const { - switch (message_type_) { - case 0: // Function pointer - return data_.ptr(entity, conn, remaining_size, is_single); - + bool is_single, uint16_t message_type) const { + if (has_tagged_string_ptr_()) { + // Handle string-based messages + switch (message_type) { #ifdef USE_EVENT - case EventResponse::MESSAGE_TYPE: { - auto *e = static_cast(entity); - return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single); - } + case EventResponse::MESSAGE_TYPE: { + auto *e = static_cast(entity); + return APIConnection::try_send_event_response(e, *get_string_ptr_(), conn, remaining_size, is_single); + } #endif - - default: - // Should not happen, return 0 to indicate no message - return 0; + default: + // Should not happen, return 0 to indicate no message + return 0; + } + } else { + // Function pointer case + return data_.ptr(entity, conn, remaining_size, is_single); } } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index da12a3e449..e872711e95 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -483,55 +483,57 @@ class APIConnection : public APIServerConnection { // Function pointer type for message encoding using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); - // Optimized MessageCreator class using union dispatch + // Optimized MessageCreator class using tagged pointer class MessageCreator { + // Ensure pointer alignment allows LSB tagging + static_assert(alignof(std::string *) > 1, "String pointer alignment must be > 1 for LSB tagging"); + public: - // Constructor for function pointer (message_type = 0) - MessageCreator(MessageCreatorPtr ptr) : message_type_(0) { data_.ptr = ptr; } + // Constructor for function pointer + MessageCreator(MessageCreatorPtr ptr) { + // Function pointers are always aligned, so LSB is 0 + data_.ptr = ptr; + } // Constructor for string state capture - MessageCreator(const std::string &value, uint16_t msg_type) : message_type_(msg_type) { - data_.string_ptr = new std::string(value); + explicit MessageCreator(const std::string &str_value) { + // Allocate string and tag the pointer + auto *str = new std::string(str_value); + // Set LSB to 1 to indicate string pointer + data_.tagged = reinterpret_cast(str) | 1; } // Destructor ~MessageCreator() { - // Clean up string data for string-based message types - if (uses_string_data_()) { - delete data_.string_ptr; + if (has_tagged_string_ptr_()) { + delete get_string_ptr_(); } } // Copy constructor - MessageCreator(const MessageCreator &other) : message_type_(other.message_type_) { - if (message_type_ == 0) { - data_.ptr = other.data_.ptr; - } else if (uses_string_data_()) { - data_.string_ptr = new std::string(*other.data_.string_ptr); + MessageCreator(const MessageCreator &other) { + if (other.has_tagged_string_ptr_()) { + auto *str = new std::string(*other.get_string_ptr_()); + data_.tagged = reinterpret_cast(str) | 1; } else { - data_ = other.data_; // For POD types + data_ = other.data_; } } // Move constructor - MessageCreator(MessageCreator &&other) noexcept : data_(other.data_), message_type_(other.message_type_) { - other.message_type_ = 0; // Reset other to function pointer type - other.data_.ptr = nullptr; - } + MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.ptr = nullptr; } // Assignment operators (needed for batch deduplication) MessageCreator &operator=(const MessageCreator &other) { if (this != &other) { // Clean up current string data if needed - if (uses_string_data_()) { - delete data_.string_ptr; + if (has_tagged_string_ptr_()) { + delete get_string_ptr_(); } // Copy new data - message_type_ = other.message_type_; - if (other.message_type_ == 0) { - data_.ptr = other.data_.ptr; - } else if (other.uses_string_data_()) { - data_.string_ptr = new std::string(*other.data_.string_ptr); + if (other.has_tagged_string_ptr_()) { + auto *str = new std::string(*other.get_string_ptr_()); + data_.tagged = reinterpret_cast(str) | 1; } else { data_ = other.data_; } @@ -542,30 +544,35 @@ class APIConnection : public APIServerConnection { MessageCreator &operator=(MessageCreator &&other) noexcept { if (this != &other) { // Clean up current string data if needed - if (uses_string_data_()) { - delete data_.string_ptr; + if (has_tagged_string_ptr_()) { + delete get_string_ptr_(); } // Move data - message_type_ = other.message_type_; data_ = other.data_; // Reset other to safe state - other.message_type_ = 0; other.data_.ptr = nullptr; } return *this; } - // Call operator - uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) const; + // Call operator - now accepts message_type as parameter + uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, + uint16_t message_type) const; private: - // Helper to check if this message type uses heap-allocated strings - bool uses_string_data_() const { return message_type_ == EventResponse::MESSAGE_TYPE; } - union CreatorData { - MessageCreatorPtr ptr; // 8 bytes - std::string *string_ptr; // 8 bytes - } data_; // 8 bytes - uint16_t message_type_; // 2 bytes (0 = function ptr, >0 = state capture) + // Check if this contains a string pointer + bool has_tagged_string_ptr_() const { return (data_.tagged & 1) != 0; } + + // Get the actual string pointer (clears the tag bit) + std::string *get_string_ptr_() const { + // NOLINTNEXTLINE(performance-no-int-to-ptr) + return reinterpret_cast(data_.tagged & ~uintptr_t(1)); + } + + union { + MessageCreatorPtr ptr; + uintptr_t tagged; + } data_; // 4 bytes on 32-bit }; // Generic batching mechanism for both state updates and entity info From f029f4f20e8074a057fba889720fdc410c30764b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Jun 2025 03:57:41 +0200 Subject: [PATCH 122/293] Fix missing protobuf message dump for batched messages with very verbose logging (#9206) --- esphome/components/api/api_connection.cpp | 5 + esphome/components/api/api_pb2.h | 254 +++++++++++----------- esphome/components/api/api_pb2_service.h | 2 +- esphome/components/api/proto.h | 1 + script/api_protobuf/api_protobuf.py | 4 +- 5 files changed, 136 insertions(+), 130 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index b7f6f04be0..fdcce6088c 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -276,6 +276,11 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes // Encode directly into buffer msg.encode(buffer); +#ifdef HAS_PROTO_MESSAGE_DUMP + // Log the message for VV debugging + conn->log_send_message_(msg.message_name(), msg.dump()); +#endif + // Calculate actual encoded size (not including header that was already added) size_t actual_payload_size = shared_buf.size() - size_before_encode; diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 6a5b51d3a1..2f0444c2cd 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -281,7 +281,7 @@ class HelloRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 1; static constexpr uint16_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "hello_request"; } + const char *message_name() const override { return "hello_request"; } #endif std::string client_info{}; uint32_t api_version_major{0}; @@ -301,7 +301,7 @@ class HelloResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 2; static constexpr uint16_t ESTIMATED_SIZE = 26; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "hello_response"; } + const char *message_name() const override { return "hello_response"; } #endif uint32_t api_version_major{0}; uint32_t api_version_minor{0}; @@ -322,7 +322,7 @@ class ConnectRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 3; static constexpr uint16_t ESTIMATED_SIZE = 9; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "connect_request"; } + const char *message_name() const override { return "connect_request"; } #endif std::string password{}; void encode(ProtoWriteBuffer buffer) const override; @@ -339,7 +339,7 @@ class ConnectResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 4; static constexpr uint16_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "connect_response"; } + const char *message_name() const override { return "connect_response"; } #endif bool invalid_password{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -356,7 +356,7 @@ class DisconnectRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 5; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "disconnect_request"; } + const char *message_name() const override { return "disconnect_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -369,7 +369,7 @@ class DisconnectResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 6; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "disconnect_response"; } + const char *message_name() const override { return "disconnect_response"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -382,7 +382,7 @@ class PingRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 7; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "ping_request"; } + const char *message_name() const override { return "ping_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -395,7 +395,7 @@ class PingResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 8; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "ping_response"; } + const char *message_name() const override { return "ping_response"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -408,7 +408,7 @@ class DeviceInfoRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 9; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "device_info_request"; } + const char *message_name() const override { return "device_info_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -450,7 +450,7 @@ class DeviceInfoResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 10; static constexpr uint16_t ESTIMATED_SIZE = 219; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "device_info_response"; } + const char *message_name() const override { return "device_info_response"; } #endif bool uses_password{false}; std::string name{}; @@ -489,7 +489,7 @@ class ListEntitiesRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 11; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_request"; } + const char *message_name() const override { return "list_entities_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -502,7 +502,7 @@ class ListEntitiesDoneResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 19; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_done_response"; } + const char *message_name() const override { return "list_entities_done_response"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -515,7 +515,7 @@ class SubscribeStatesRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 20; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_states_request"; } + const char *message_name() const override { return "subscribe_states_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -528,7 +528,7 @@ class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 12; static constexpr uint16_t ESTIMATED_SIZE = 60; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_binary_sensor_response"; } + const char *message_name() const override { return "list_entities_binary_sensor_response"; } #endif std::string device_class{}; bool is_status_binary_sensor{false}; @@ -548,7 +548,7 @@ class BinarySensorStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 21; static constexpr uint16_t ESTIMATED_SIZE = 9; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "binary_sensor_state_response"; } + const char *message_name() const override { return "binary_sensor_state_response"; } #endif bool state{false}; bool missing_state{false}; @@ -567,7 +567,7 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 13; static constexpr uint16_t ESTIMATED_SIZE = 66; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_cover_response"; } + const char *message_name() const override { return "list_entities_cover_response"; } #endif bool assumed_state{false}; bool supports_position{false}; @@ -590,7 +590,7 @@ class CoverStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 22; static constexpr uint16_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "cover_state_response"; } + const char *message_name() const override { return "cover_state_response"; } #endif enums::LegacyCoverState legacy_state{}; float position{0.0f}; @@ -611,7 +611,7 @@ class CoverCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 30; static constexpr uint16_t ESTIMATED_SIZE = 25; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "cover_command_request"; } + const char *message_name() const override { return "cover_command_request"; } #endif uint32_t key{0}; bool has_legacy_command{false}; @@ -636,7 +636,7 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 14; static constexpr uint16_t ESTIMATED_SIZE = 77; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_fan_response"; } + const char *message_name() const override { return "list_entities_fan_response"; } #endif bool supports_oscillation{false}; bool supports_speed{false}; @@ -659,7 +659,7 @@ class FanStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 23; static constexpr uint16_t ESTIMATED_SIZE = 26; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "fan_state_response"; } + const char *message_name() const override { return "fan_state_response"; } #endif bool state{false}; bool oscillating{false}; @@ -683,7 +683,7 @@ class FanCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 31; static constexpr uint16_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "fan_command_request"; } + const char *message_name() const override { return "fan_command_request"; } #endif uint32_t key{0}; bool has_state{false}; @@ -714,7 +714,7 @@ class ListEntitiesLightResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 15; static constexpr uint16_t ESTIMATED_SIZE = 90; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_light_response"; } + const char *message_name() const override { return "list_entities_light_response"; } #endif std::vector supported_color_modes{}; bool legacy_supports_brightness{false}; @@ -740,7 +740,7 @@ class LightStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 24; static constexpr uint16_t ESTIMATED_SIZE = 63; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "light_state_response"; } + const char *message_name() const override { return "light_state_response"; } #endif bool state{false}; float brightness{0.0f}; @@ -770,7 +770,7 @@ class LightCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 32; static constexpr uint16_t ESTIMATED_SIZE = 107; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "light_command_request"; } + const char *message_name() const override { return "light_command_request"; } #endif uint32_t key{0}; bool has_state{false}; @@ -815,7 +815,7 @@ class ListEntitiesSensorResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 16; static constexpr uint16_t ESTIMATED_SIZE = 77; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_sensor_response"; } + const char *message_name() const override { return "list_entities_sensor_response"; } #endif std::string unit_of_measurement{}; int32_t accuracy_decimals{0}; @@ -839,7 +839,7 @@ class SensorStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 25; static constexpr uint16_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "sensor_state_response"; } + const char *message_name() const override { return "sensor_state_response"; } #endif float state{0.0f}; bool missing_state{false}; @@ -858,7 +858,7 @@ class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 17; static constexpr uint16_t ESTIMATED_SIZE = 60; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_switch_response"; } + const char *message_name() const override { return "list_entities_switch_response"; } #endif bool assumed_state{false}; std::string device_class{}; @@ -878,7 +878,7 @@ class SwitchStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 26; static constexpr uint16_t ESTIMATED_SIZE = 7; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "switch_state_response"; } + const char *message_name() const override { return "switch_state_response"; } #endif bool state{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -896,7 +896,7 @@ class SwitchCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 33; static constexpr uint16_t ESTIMATED_SIZE = 7; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "switch_command_request"; } + const char *message_name() const override { return "switch_command_request"; } #endif uint32_t key{0}; bool state{false}; @@ -915,7 +915,7 @@ class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 18; static constexpr uint16_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_text_sensor_response"; } + const char *message_name() const override { return "list_entities_text_sensor_response"; } #endif std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; @@ -934,7 +934,7 @@ class TextSensorStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 27; static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "text_sensor_state_response"; } + const char *message_name() const override { return "text_sensor_state_response"; } #endif std::string state{}; bool missing_state{false}; @@ -954,7 +954,7 @@ class SubscribeLogsRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 28; static constexpr uint16_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_logs_request"; } + const char *message_name() const override { return "subscribe_logs_request"; } #endif enums::LogLevel level{}; bool dump_config{false}; @@ -972,7 +972,7 @@ class SubscribeLogsResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 29; static constexpr uint16_t ESTIMATED_SIZE = 13; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_logs_response"; } + const char *message_name() const override { return "subscribe_logs_response"; } #endif enums::LogLevel level{}; std::string message{}; @@ -992,7 +992,7 @@ class NoiseEncryptionSetKeyRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 124; static constexpr uint16_t ESTIMATED_SIZE = 9; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "noise_encryption_set_key_request"; } + const char *message_name() const override { return "noise_encryption_set_key_request"; } #endif std::string key{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1009,7 +1009,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 125; static constexpr uint16_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "noise_encryption_set_key_response"; } + const char *message_name() const override { return "noise_encryption_set_key_response"; } #endif bool success{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -1026,7 +1026,7 @@ class SubscribeHomeassistantServicesRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 34; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_homeassistant_services_request"; } + const char *message_name() const override { return "subscribe_homeassistant_services_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1052,7 +1052,7 @@ class HomeassistantServiceResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 35; static constexpr uint16_t ESTIMATED_SIZE = 113; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "homeassistant_service_response"; } + const char *message_name() const override { return "homeassistant_service_response"; } #endif std::string service{}; std::vector data{}; @@ -1074,7 +1074,7 @@ class SubscribeHomeAssistantStatesRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 38; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_home_assistant_states_request"; } + const char *message_name() const override { return "subscribe_home_assistant_states_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1087,7 +1087,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 39; static constexpr uint16_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_home_assistant_state_response"; } + const char *message_name() const override { return "subscribe_home_assistant_state_response"; } #endif std::string entity_id{}; std::string attribute{}; @@ -1107,7 +1107,7 @@ class HomeAssistantStateResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 40; static constexpr uint16_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "home_assistant_state_response"; } + const char *message_name() const override { return "home_assistant_state_response"; } #endif std::string entity_id{}; std::string state{}; @@ -1126,7 +1126,7 @@ class GetTimeRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 36; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "get_time_request"; } + const char *message_name() const override { return "get_time_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1139,7 +1139,7 @@ class GetTimeResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 37; static constexpr uint16_t ESTIMATED_SIZE = 5; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "get_time_response"; } + const char *message_name() const override { return "get_time_response"; } #endif uint32_t epoch_seconds{0}; void encode(ProtoWriteBuffer buffer) const override; @@ -1170,7 +1170,7 @@ class ListEntitiesServicesResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 41; static constexpr uint16_t ESTIMATED_SIZE = 48; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_services_response"; } + const char *message_name() const override { return "list_entities_services_response"; } #endif std::string name{}; uint32_t key{0}; @@ -1212,7 +1212,7 @@ class ExecuteServiceRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 42; static constexpr uint16_t ESTIMATED_SIZE = 39; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "execute_service_request"; } + const char *message_name() const override { return "execute_service_request"; } #endif uint32_t key{0}; std::vector args{}; @@ -1231,7 +1231,7 @@ class ListEntitiesCameraResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 43; static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_camera_response"; } + const char *message_name() const override { return "list_entities_camera_response"; } #endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -1249,7 +1249,7 @@ class CameraImageResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 44; static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "camera_image_response"; } + const char *message_name() const override { return "camera_image_response"; } #endif uint32_t key{0}; std::string data{}; @@ -1270,7 +1270,7 @@ class CameraImageRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 45; static constexpr uint16_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "camera_image_request"; } + const char *message_name() const override { return "camera_image_request"; } #endif bool single{false}; bool stream{false}; @@ -1288,7 +1288,7 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 46; static constexpr uint16_t ESTIMATED_SIZE = 156; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_climate_response"; } + const char *message_name() const override { return "list_entities_climate_response"; } #endif bool supports_current_temperature{false}; bool supports_two_point_target_temperature{false}; @@ -1324,7 +1324,7 @@ class ClimateStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 47; static constexpr uint16_t ESTIMATED_SIZE = 65; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "climate_state_response"; } + const char *message_name() const override { return "climate_state_response"; } #endif enums::ClimateMode mode{}; float current_temperature{0.0f}; @@ -1356,7 +1356,7 @@ class ClimateCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 48; static constexpr uint16_t ESTIMATED_SIZE = 83; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "climate_command_request"; } + const char *message_name() const override { return "climate_command_request"; } #endif uint32_t key{0}; bool has_mode{false}; @@ -1397,7 +1397,7 @@ class ListEntitiesNumberResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 49; static constexpr uint16_t ESTIMATED_SIZE = 84; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_number_response"; } + const char *message_name() const override { return "list_entities_number_response"; } #endif float min_value{0.0f}; float max_value{0.0f}; @@ -1421,7 +1421,7 @@ class NumberStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 50; static constexpr uint16_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "number_state_response"; } + const char *message_name() const override { return "number_state_response"; } #endif float state{0.0f}; bool missing_state{false}; @@ -1440,7 +1440,7 @@ class NumberCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 51; static constexpr uint16_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "number_command_request"; } + const char *message_name() const override { return "number_command_request"; } #endif uint32_t key{0}; float state{0.0f}; @@ -1458,7 +1458,7 @@ class ListEntitiesSelectResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 52; static constexpr uint16_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_select_response"; } + const char *message_name() const override { return "list_entities_select_response"; } #endif std::vector options{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1477,7 +1477,7 @@ class SelectStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 53; static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "select_state_response"; } + const char *message_name() const override { return "select_state_response"; } #endif std::string state{}; bool missing_state{false}; @@ -1497,7 +1497,7 @@ class SelectCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 54; static constexpr uint16_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "select_command_request"; } + const char *message_name() const override { return "select_command_request"; } #endif uint32_t key{0}; std::string state{}; @@ -1516,7 +1516,7 @@ class ListEntitiesSirenResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 55; static constexpr uint16_t ESTIMATED_SIZE = 71; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_siren_response"; } + const char *message_name() const override { return "list_entities_siren_response"; } #endif std::vector tones{}; bool supports_duration{false}; @@ -1537,7 +1537,7 @@ class SirenStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 56; static constexpr uint16_t ESTIMATED_SIZE = 7; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "siren_state_response"; } + const char *message_name() const override { return "siren_state_response"; } #endif bool state{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -1555,7 +1555,7 @@ class SirenCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 57; static constexpr uint16_t ESTIMATED_SIZE = 33; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "siren_command_request"; } + const char *message_name() const override { return "siren_command_request"; } #endif uint32_t key{0}; bool has_state{false}; @@ -1582,7 +1582,7 @@ class ListEntitiesLockResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 58; static constexpr uint16_t ESTIMATED_SIZE = 64; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_lock_response"; } + const char *message_name() const override { return "list_entities_lock_response"; } #endif bool assumed_state{false}; bool supports_open{false}; @@ -1604,7 +1604,7 @@ class LockStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 59; static constexpr uint16_t ESTIMATED_SIZE = 7; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "lock_state_response"; } + const char *message_name() const override { return "lock_state_response"; } #endif enums::LockState state{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1622,7 +1622,7 @@ class LockCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 60; static constexpr uint16_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "lock_command_request"; } + const char *message_name() const override { return "lock_command_request"; } #endif uint32_t key{0}; enums::LockCommand command{}; @@ -1644,7 +1644,7 @@ class ListEntitiesButtonResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 61; static constexpr uint16_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_button_response"; } + const char *message_name() const override { return "list_entities_button_response"; } #endif std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1663,7 +1663,7 @@ class ButtonCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 62; static constexpr uint16_t ESTIMATED_SIZE = 5; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "button_command_request"; } + const char *message_name() const override { return "button_command_request"; } #endif uint32_t key{0}; void encode(ProtoWriteBuffer buffer) const override; @@ -1697,7 +1697,7 @@ class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 63; static constexpr uint16_t ESTIMATED_SIZE = 85; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_media_player_response"; } + const char *message_name() const override { return "list_entities_media_player_response"; } #endif bool supports_pause{false}; std::vector supported_formats{}; @@ -1717,7 +1717,7 @@ class MediaPlayerStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 64; static constexpr uint16_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "media_player_state_response"; } + const char *message_name() const override { return "media_player_state_response"; } #endif enums::MediaPlayerState state{}; float volume{0.0f}; @@ -1737,7 +1737,7 @@ class MediaPlayerCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 65; static constexpr uint16_t ESTIMATED_SIZE = 31; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "media_player_command_request"; } + const char *message_name() const override { return "media_player_command_request"; } #endif uint32_t key{0}; bool has_command{false}; @@ -1764,7 +1764,7 @@ class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 66; static constexpr uint16_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_bluetooth_le_advertisements_request"; } + const char *message_name() const override { return "subscribe_bluetooth_le_advertisements_request"; } #endif uint32_t flags{0}; void encode(ProtoWriteBuffer buffer) const override; @@ -1796,7 +1796,7 @@ class BluetoothLEAdvertisementResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 67; static constexpr uint16_t ESTIMATED_SIZE = 107; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_le_advertisement_response"; } + const char *message_name() const override { return "bluetooth_le_advertisement_response"; } #endif uint64_t address{0}; std::string name{}; @@ -1836,7 +1836,7 @@ class BluetoothLERawAdvertisementsResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 93; static constexpr uint16_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_le_raw_advertisements_response"; } + const char *message_name() const override { return "bluetooth_le_raw_advertisements_response"; } #endif std::vector advertisements{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1853,7 +1853,7 @@ class BluetoothDeviceRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 68; static constexpr uint16_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_device_request"; } + const char *message_name() const override { return "bluetooth_device_request"; } #endif uint64_t address{0}; enums::BluetoothDeviceRequestType request_type{}; @@ -1873,7 +1873,7 @@ class BluetoothDeviceConnectionResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 69; static constexpr uint16_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_device_connection_response"; } + const char *message_name() const override { return "bluetooth_device_connection_response"; } #endif uint64_t address{0}; bool connected{false}; @@ -1893,7 +1893,7 @@ class BluetoothGATTGetServicesRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 70; static constexpr uint16_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_get_services_request"; } + const char *message_name() const override { return "bluetooth_gatt_get_services_request"; } #endif uint64_t address{0}; void encode(ProtoWriteBuffer buffer) const override; @@ -1954,7 +1954,7 @@ class BluetoothGATTGetServicesResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 71; static constexpr uint16_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_get_services_response"; } + const char *message_name() const override { return "bluetooth_gatt_get_services_response"; } #endif uint64_t address{0}; std::vector services{}; @@ -1973,7 +1973,7 @@ class BluetoothGATTGetServicesDoneResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 72; static constexpr uint16_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_get_services_done_response"; } + const char *message_name() const override { return "bluetooth_gatt_get_services_done_response"; } #endif uint64_t address{0}; void encode(ProtoWriteBuffer buffer) const override; @@ -1990,7 +1990,7 @@ class BluetoothGATTReadRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 73; static constexpr uint16_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_read_request"; } + const char *message_name() const override { return "bluetooth_gatt_read_request"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2008,7 +2008,7 @@ class BluetoothGATTReadResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 74; static constexpr uint16_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_read_response"; } + const char *message_name() const override { return "bluetooth_gatt_read_response"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2028,7 +2028,7 @@ class BluetoothGATTWriteRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 75; static constexpr uint16_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_write_request"; } + const char *message_name() const override { return "bluetooth_gatt_write_request"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2049,7 +2049,7 @@ class BluetoothGATTReadDescriptorRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 76; static constexpr uint16_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_read_descriptor_request"; } + const char *message_name() const override { return "bluetooth_gatt_read_descriptor_request"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2067,7 +2067,7 @@ class BluetoothGATTWriteDescriptorRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 77; static constexpr uint16_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_write_descriptor_request"; } + const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2087,7 +2087,7 @@ class BluetoothGATTNotifyRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 78; static constexpr uint16_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_notify_request"; } + const char *message_name() const override { return "bluetooth_gatt_notify_request"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2106,7 +2106,7 @@ class BluetoothGATTNotifyDataResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 79; static constexpr uint16_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_notify_data_response"; } + const char *message_name() const override { return "bluetooth_gatt_notify_data_response"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2126,7 +2126,7 @@ class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 80; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_bluetooth_connections_free_request"; } + const char *message_name() const override { return "subscribe_bluetooth_connections_free_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -2139,7 +2139,7 @@ class BluetoothConnectionsFreeResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 81; static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_connections_free_response"; } + const char *message_name() const override { return "bluetooth_connections_free_response"; } #endif uint32_t free{0}; uint32_t limit{0}; @@ -2158,7 +2158,7 @@ class BluetoothGATTErrorResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 82; static constexpr uint16_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_error_response"; } + const char *message_name() const override { return "bluetooth_gatt_error_response"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2177,7 +2177,7 @@ class BluetoothGATTWriteResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 83; static constexpr uint16_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_write_response"; } + const char *message_name() const override { return "bluetooth_gatt_write_response"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2195,7 +2195,7 @@ class BluetoothGATTNotifyResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 84; static constexpr uint16_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_notify_response"; } + const char *message_name() const override { return "bluetooth_gatt_notify_response"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2213,7 +2213,7 @@ class BluetoothDevicePairingResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 85; static constexpr uint16_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_device_pairing_response"; } + const char *message_name() const override { return "bluetooth_device_pairing_response"; } #endif uint64_t address{0}; bool paired{false}; @@ -2232,7 +2232,7 @@ class BluetoothDeviceUnpairingResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 86; static constexpr uint16_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_device_unpairing_response"; } + const char *message_name() const override { return "bluetooth_device_unpairing_response"; } #endif uint64_t address{0}; bool success{false}; @@ -2251,7 +2251,7 @@ class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 87; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "unsubscribe_bluetooth_le_advertisements_request"; } + const char *message_name() const override { return "unsubscribe_bluetooth_le_advertisements_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -2264,7 +2264,7 @@ class BluetoothDeviceClearCacheResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 88; static constexpr uint16_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_device_clear_cache_response"; } + const char *message_name() const override { return "bluetooth_device_clear_cache_response"; } #endif uint64_t address{0}; bool success{false}; @@ -2283,7 +2283,7 @@ class BluetoothScannerStateResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 126; static constexpr uint16_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_scanner_state_response"; } + const char *message_name() const override { return "bluetooth_scanner_state_response"; } #endif enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; @@ -2301,7 +2301,7 @@ class BluetoothScannerSetModeRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 127; static constexpr uint16_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_scanner_set_mode_request"; } + const char *message_name() const override { return "bluetooth_scanner_set_mode_request"; } #endif enums::BluetoothScannerMode mode{}; void encode(ProtoWriteBuffer buffer) const override; @@ -2318,7 +2318,7 @@ class SubscribeVoiceAssistantRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 89; static constexpr uint16_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_voice_assistant_request"; } + const char *message_name() const override { return "subscribe_voice_assistant_request"; } #endif bool subscribe{false}; uint32_t flags{0}; @@ -2351,7 +2351,7 @@ class VoiceAssistantRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 90; static constexpr uint16_t ESTIMATED_SIZE = 41; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_request"; } + const char *message_name() const override { return "voice_assistant_request"; } #endif bool start{false}; std::string conversation_id{}; @@ -2373,7 +2373,7 @@ class VoiceAssistantResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 91; static constexpr uint16_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_response"; } + const char *message_name() const override { return "voice_assistant_response"; } #endif uint32_t port{0}; bool error{false}; @@ -2404,7 +2404,7 @@ class VoiceAssistantEventResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 92; static constexpr uint16_t ESTIMATED_SIZE = 36; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_event_response"; } + const char *message_name() const override { return "voice_assistant_event_response"; } #endif enums::VoiceAssistantEvent event_type{}; std::vector data{}; @@ -2423,7 +2423,7 @@ class VoiceAssistantAudio : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 106; static constexpr uint16_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_audio"; } + const char *message_name() const override { return "voice_assistant_audio"; } #endif std::string data{}; bool end{false}; @@ -2442,7 +2442,7 @@ class VoiceAssistantTimerEventResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 115; static constexpr uint16_t ESTIMATED_SIZE = 30; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_timer_event_response"; } + const char *message_name() const override { return "voice_assistant_timer_event_response"; } #endif enums::VoiceAssistantTimerEvent event_type{}; std::string timer_id{}; @@ -2465,7 +2465,7 @@ class VoiceAssistantAnnounceRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 119; static constexpr uint16_t ESTIMATED_SIZE = 29; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_announce_request"; } + const char *message_name() const override { return "voice_assistant_announce_request"; } #endif std::string media_id{}; std::string text{}; @@ -2486,7 +2486,7 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 120; static constexpr uint16_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_announce_finished"; } + const char *message_name() const override { return "voice_assistant_announce_finished"; } #endif bool success{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -2517,7 +2517,7 @@ class VoiceAssistantConfigurationRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 121; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_configuration_request"; } + const char *message_name() const override { return "voice_assistant_configuration_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -2530,7 +2530,7 @@ class VoiceAssistantConfigurationResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 122; static constexpr uint16_t ESTIMATED_SIZE = 56; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_configuration_response"; } + const char *message_name() const override { return "voice_assistant_configuration_response"; } #endif std::vector available_wake_words{}; std::vector active_wake_words{}; @@ -2550,7 +2550,7 @@ class VoiceAssistantSetConfiguration : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 123; static constexpr uint16_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_set_configuration"; } + const char *message_name() const override { return "voice_assistant_set_configuration"; } #endif std::vector active_wake_words{}; void encode(ProtoWriteBuffer buffer) const override; @@ -2567,7 +2567,7 @@ class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 94; static constexpr uint16_t ESTIMATED_SIZE = 57; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_alarm_control_panel_response"; } + const char *message_name() const override { return "list_entities_alarm_control_panel_response"; } #endif uint32_t supported_features{0}; bool requires_code{false}; @@ -2588,7 +2588,7 @@ class AlarmControlPanelStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 95; static constexpr uint16_t ESTIMATED_SIZE = 7; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "alarm_control_panel_state_response"; } + const char *message_name() const override { return "alarm_control_panel_state_response"; } #endif enums::AlarmControlPanelState state{}; void encode(ProtoWriteBuffer buffer) const override; @@ -2606,7 +2606,7 @@ class AlarmControlPanelCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 96; static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "alarm_control_panel_command_request"; } + const char *message_name() const override { return "alarm_control_panel_command_request"; } #endif uint32_t key{0}; enums::AlarmControlPanelStateCommand command{}; @@ -2627,7 +2627,7 @@ class ListEntitiesTextResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 97; static constexpr uint16_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_text_response"; } + const char *message_name() const override { return "list_entities_text_response"; } #endif uint32_t min_length{0}; uint32_t max_length{0}; @@ -2649,7 +2649,7 @@ class TextStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 98; static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "text_state_response"; } + const char *message_name() const override { return "text_state_response"; } #endif std::string state{}; bool missing_state{false}; @@ -2669,7 +2669,7 @@ class TextCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 99; static constexpr uint16_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "text_command_request"; } + const char *message_name() const override { return "text_command_request"; } #endif uint32_t key{0}; std::string state{}; @@ -2688,7 +2688,7 @@ class ListEntitiesDateResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 100; static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_date_response"; } + const char *message_name() const override { return "list_entities_date_response"; } #endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -2706,7 +2706,7 @@ class DateStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 101; static constexpr uint16_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "date_state_response"; } + const char *message_name() const override { return "date_state_response"; } #endif bool missing_state{false}; uint32_t year{0}; @@ -2727,7 +2727,7 @@ class DateCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 102; static constexpr uint16_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "date_command_request"; } + const char *message_name() const override { return "date_command_request"; } #endif uint32_t key{0}; uint32_t year{0}; @@ -2748,7 +2748,7 @@ class ListEntitiesTimeResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 103; static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_time_response"; } + const char *message_name() const override { return "list_entities_time_response"; } #endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -2766,7 +2766,7 @@ class TimeStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 104; static constexpr uint16_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "time_state_response"; } + const char *message_name() const override { return "time_state_response"; } #endif bool missing_state{false}; uint32_t hour{0}; @@ -2787,7 +2787,7 @@ class TimeCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 105; static constexpr uint16_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "time_command_request"; } + const char *message_name() const override { return "time_command_request"; } #endif uint32_t key{0}; uint32_t hour{0}; @@ -2808,7 +2808,7 @@ class ListEntitiesEventResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 107; static constexpr uint16_t ESTIMATED_SIZE = 76; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_event_response"; } + const char *message_name() const override { return "list_entities_event_response"; } #endif std::string device_class{}; std::vector event_types{}; @@ -2828,7 +2828,7 @@ class EventResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 108; static constexpr uint16_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "event_response"; } + const char *message_name() const override { return "event_response"; } #endif std::string event_type{}; void encode(ProtoWriteBuffer buffer) const override; @@ -2846,7 +2846,7 @@ class ListEntitiesValveResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 109; static constexpr uint16_t ESTIMATED_SIZE = 64; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_valve_response"; } + const char *message_name() const override { return "list_entities_valve_response"; } #endif std::string device_class{}; bool assumed_state{false}; @@ -2868,7 +2868,7 @@ class ValveStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 110; static constexpr uint16_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "valve_state_response"; } + const char *message_name() const override { return "valve_state_response"; } #endif float position{0.0f}; enums::ValveOperation current_operation{}; @@ -2887,7 +2887,7 @@ class ValveCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 111; static constexpr uint16_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "valve_command_request"; } + const char *message_name() const override { return "valve_command_request"; } #endif uint32_t key{0}; bool has_position{false}; @@ -2908,7 +2908,7 @@ class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 112; static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_date_time_response"; } + const char *message_name() const override { return "list_entities_date_time_response"; } #endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -2926,7 +2926,7 @@ class DateTimeStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 113; static constexpr uint16_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "date_time_state_response"; } + const char *message_name() const override { return "date_time_state_response"; } #endif bool missing_state{false}; uint32_t epoch_seconds{0}; @@ -2945,7 +2945,7 @@ class DateTimeCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 114; static constexpr uint16_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "date_time_command_request"; } + const char *message_name() const override { return "date_time_command_request"; } #endif uint32_t key{0}; uint32_t epoch_seconds{0}; @@ -2963,7 +2963,7 @@ class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 116; static constexpr uint16_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_update_response"; } + const char *message_name() const override { return "list_entities_update_response"; } #endif std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; @@ -2982,7 +2982,7 @@ class UpdateStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 117; static constexpr uint16_t ESTIMATED_SIZE = 61; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "update_state_response"; } + const char *message_name() const override { return "update_state_response"; } #endif bool missing_state{false}; bool in_progress{false}; @@ -3009,7 +3009,7 @@ class UpdateCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 118; static constexpr uint16_t ESTIMATED_SIZE = 7; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "update_command_request"; } + const char *message_name() const override { return "update_command_request"; } #endif uint32_t key{0}; enums::UpdateCommand command{}; diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index b2be314aaf..047c56198a 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -19,7 +19,7 @@ class APIServerConnectionBase : public ProtoService { template bool send_message(const T &msg) { #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_send_message_(T::message_name(), msg.dump()); + this->log_send_message_(msg.message_name(), msg.dump()); #endif return this->send_message_(msg, T::MESSAGE_TYPE); } diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index e850236db6..d9c9e3c85d 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -335,6 +335,7 @@ class ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP std::string dump() const; virtual void dump_to(std::string &out) const = 0; + virtual const char *message_name() const { return "unknown"; } #endif protected: diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index bd1be66649..419b5aa97d 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -886,7 +886,7 @@ def build_message_type( public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP") snake_name = camel_to_snake(desc.name) public_content.append( - f'static constexpr const char *message_name() {{ return "{snake_name}"; }}' + f'const char *message_name() const override {{ return "{snake_name}"; }}' ) public_content.append("#endif") @@ -1356,7 +1356,7 @@ def main() -> None: hpp += " template\n" hpp += " bool send_message(const T &msg) {\n" hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" - hpp += " this->log_send_message_(T::message_name(), msg.dump());\n" + hpp += " this->log_send_message_(msg.message_name(), msg.dump());\n" hpp += "#endif\n" hpp += " return this->send_message_(msg, T::MESSAGE_TYPE);\n" hpp += " }\n\n" From 23b1e428dec69e4743b79d604200c17b1608fbe5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Jun 2025 05:35:01 +0200 Subject: [PATCH 123/293] Optimize Application class memory layout and reduce loop_interval size (#9208) --- esphome/core/application.h | 66 ++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 17270ca459..6ee05309ca 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include #include "esphome/core/component.h" @@ -335,11 +337,16 @@ class Application { * Each component can request a high frequency loop execution by using the HighFrequencyLoopRequester * helper in helpers.h * + * Note: This method is not called by ESPHome core code. It is only used by lambda functions + * in YAML configurations or by external components. + * * @param loop_interval The interval in milliseconds to run the core loop at. Defaults to 16 milliseconds. */ - void set_loop_interval(uint32_t loop_interval) { this->loop_interval_ = loop_interval; } + void set_loop_interval(uint32_t loop_interval) { + this->loop_interval_ = std::min(loop_interval, static_cast(std::numeric_limits::max())); + } - uint32_t get_loop_interval() const { return this->loop_interval_; } + uint32_t get_loop_interval() const { return static_cast(this->loop_interval_); } void schedule_dump_config() { this->dump_config_at_ = 0; } @@ -618,6 +625,17 @@ class Application { /// Perform a delay while also monitoring socket file descriptors for readiness void yield_with_select_(uint32_t delay_ms); + // === Member variables ordered by size to minimize padding === + + // Pointer-sized members first + Component *current_component_{nullptr}; + const char *comment_{nullptr}; + const char *compilation_time_{nullptr}; + + // size_t members + size_t dump_config_at_{SIZE_MAX}; + + // Vectors (largest members) std::vector components_{}; // Partitioned vector design for looping components @@ -637,11 +655,6 @@ class Application { // and active_end_ is incremented // - This eliminates branch mispredictions from flag checking in the hot loop std::vector looping_components_{}; - uint16_t looping_components_active_end_{0}; - - // For safe reentrant modifications during iteration - uint16_t current_loop_index_{0}; - bool in_loop_{false}; #ifdef USE_DEVICES std::vector devices_{}; @@ -713,26 +726,39 @@ class Application { std::vector updates_{}; #endif +#ifdef USE_SOCKET_SELECT_SUPPORT + std::vector socket_fds_; // Vector of all monitored socket file descriptors +#endif + + // String members std::string name_; std::string friendly_name_; - const char *comment_{nullptr}; - const char *compilation_time_{nullptr}; - bool name_add_mac_suffix_; + + // 4-byte members uint32_t last_loop_{0}; - uint32_t loop_interval_{16}; - size_t dump_config_at_{SIZE_MAX}; - uint8_t app_state_{0}; - volatile bool has_pending_enable_loop_requests_{false}; - Component *current_component_{nullptr}; uint32_t loop_component_start_time_{0}; #ifdef USE_SOCKET_SELECT_SUPPORT - // Socket select management - std::vector socket_fds_; // Vector of all monitored socket file descriptors + int max_fd_{-1}; // Highest file descriptor number for select() +#endif + + // 2-byte members (grouped together for alignment) + uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds) + uint16_t looping_components_active_end_{0}; + uint16_t current_loop_index_{0}; // For safe reentrant modifications during iteration + + // 1-byte members (grouped together to minimize padding) + uint8_t app_state_{0}; + bool name_add_mac_suffix_; + bool in_loop_{false}; + volatile bool has_pending_enable_loop_requests_{false}; + +#ifdef USE_SOCKET_SELECT_SUPPORT bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes - int max_fd_{-1}; // Highest file descriptor number for select() - fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes - fd_set read_fds_{}; // Working fd_set for select(), copied from base_read_fds_ + + // Variable-sized members at end + fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes + fd_set read_fds_{}; // Working fd_set for select(), copied from base_read_fds_ #endif }; From 9daa9a6de847f9b7fd7ee0c95eabe2be28ff5517 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 26 Jun 2025 16:21:51 +1200 Subject: [PATCH 124/293] Use shared workflow for locking (#9211) --- .github/workflows/lock.yml | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index ee10f49f61..4d7c86deee 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -1,28 +1,11 @@ --- -name: Lock +name: Lock closed issues and PRs on: schedule: - - cron: "30 0 * * *" + - cron: "30 0 * * *" # Run daily at 00:30 UTC workflow_dispatch: -permissions: - issues: write - pull-requests: write - -concurrency: - group: lock - jobs: lock: - runs-on: ubuntu-latest - steps: - - uses: dessant/lock-threads@v5.0.1 - with: - pr-inactive-days: "1" - pr-lock-reason: "" - exclude-any-pr-labels: keep-open - - issue-inactive-days: "7" - issue-lock-reason: "" - exclude-any-issue-labels: keep-open + uses: esphome/workflows/.github/workflows/lock.yml@main From 92365f133d384ec1b8781bd1917286a7b228b140 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 26 Jun 2025 01:29:42 -0400 Subject: [PATCH 125/293] [esp32] Improve and simplify IDF component support (#9163) --- esphome/components/esp32/__init__.py | 166 +++++++++++---------------- 1 file changed, 70 insertions(+), 96 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f179c315f9..4e2a6ab852 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -4,7 +4,7 @@ import logging import os from pathlib import Path -from esphome import git +from esphome import yaml_util import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( @@ -23,7 +23,6 @@ from esphome.const import ( CONF_REFRESH, CONF_SOURCE, CONF_TYPE, - CONF_URL, CONF_VARIANT, CONF_VERSION, KEY_CORE, @@ -32,14 +31,13 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_ESP32, - TYPE_GIT, - TYPE_LOCAL, __version__, ) from esphome.core import CORE, HexInt, TimePeriod from esphome.cpp_generator import RawExpression import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed +from esphome.types import ConfigType from .boards import BOARDS from .const import ( # noqa @@ -49,10 +47,8 @@ from .const import ( # noqa KEY_EXTRA_BUILD_FILES, KEY_PATH, KEY_REF, - KEY_REFRESH, KEY_REPO, KEY_SDKCONFIG_OPTIONS, - KEY_SUBMODULES, KEY_VARIANT, VARIANT_ESP32, VARIANT_ESP32C2, @@ -235,7 +231,7 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): def add_idf_component( *, name: str, - repo: str, + repo: str = None, ref: str = None, path: str = None, refresh: TimePeriod = None, @@ -245,30 +241,27 @@ def add_idf_component( """Add an esp-idf component to the project.""" if not CORE.using_esp_idf: raise ValueError("Not an esp-idf project") - if components is None: - components = [] - if name not in CORE.data[KEY_ESP32][KEY_COMPONENTS]: + if not repo and not ref and not path: + raise ValueError("Requires at least one of repo, ref or path") + if refresh or submodules or components: + _LOGGER.warning( + "The refresh, components and submodules parameters in add_idf_component() are " + "deprecated and will be removed in ESPHome 2026.1. If you are seeing this, report " + "an issue to the external_component author and ask them to update it." + ) + if components: + for comp in components: + CORE.data[KEY_ESP32][KEY_COMPONENTS][comp] = { + KEY_REPO: repo, + KEY_REF: ref, + KEY_PATH: f"{path}/{comp}" if path else comp, + } + else: CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = { KEY_REPO: repo, KEY_REF: ref, KEY_PATH: path, - KEY_REFRESH: refresh, - KEY_COMPONENTS: components, - KEY_SUBMODULES: submodules, } - else: - component_config = CORE.data[KEY_ESP32][KEY_COMPONENTS][name] - if components is not None: - component_config[KEY_COMPONENTS] = list( - set(component_config[KEY_COMPONENTS] + components) - ) - if submodules is not None: - if component_config[KEY_SUBMODULES] is None: - component_config[KEY_SUBMODULES] = submodules - else: - component_config[KEY_SUBMODULES] = list( - set(component_config[KEY_SUBMODULES] + submodules) - ) def add_extra_script(stage: str, filename: str, path: str): @@ -575,6 +568,17 @@ CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server" CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries" CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" + +def _validate_idf_component(config: ConfigType) -> ConfigType: + """Validate IDF component config and warn about deprecated options.""" + if CONF_REFRESH in config: + _LOGGER.warning( + "The 'refresh' option for IDF components is deprecated and has no effect. " + "It will be removed in ESPHome 2026.1. Please remove it from your configuration." + ) + return config + + ESP_IDF_FRAMEWORK_SCHEMA = cv.All( cv.Schema( { @@ -614,15 +618,19 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( } ), cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( - cv.Schema( - { - cv.Required(CONF_NAME): cv.string_strict, - cv.Required(CONF_SOURCE): cv.SOURCE_SCHEMA, - cv.Optional(CONF_PATH): cv.string, - cv.Optional(CONF_REFRESH, default="1d"): cv.All( - cv.string, cv.source_refresh - ), - } + cv.All( + cv.Schema( + { + cv.Required(CONF_NAME): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.git_ref, + cv.Optional(CONF_REF): cv.string, + cv.Optional(CONF_PATH): cv.string, + cv.Optional(CONF_REFRESH): cv.All( + cv.string, cv.source_refresh + ), + } + ), + _validate_idf_component, ) ), } @@ -814,18 +822,12 @@ async def to_code(config): add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) for component in conf[CONF_COMPONENTS]: - source = component[CONF_SOURCE] - if source[CONF_TYPE] == TYPE_GIT: - add_idf_component( - name=component[CONF_NAME], - repo=source[CONF_URL], - ref=source.get(CONF_REF), - path=component.get(CONF_PATH), - refresh=component[CONF_REFRESH], - ) - elif source[CONF_TYPE] == TYPE_LOCAL: - _LOGGER.warning("Local components are not implemented yet.") - + add_idf_component( + name=component[CONF_NAME], + repo=component.get(CONF_SOURCE), + ref=component.get(CONF_REF), + path=component.get(CONF_PATH), + ) elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") @@ -924,6 +926,26 @@ def _write_sdkconfig(): write_file_if_changed(sdk_path, contents) +def _write_idf_component_yml(): + yml_path = Path(CORE.relative_build_path("src/idf_component.yml")) + if CORE.data[KEY_ESP32][KEY_COMPONENTS]: + components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] + dependencies = {} + for name, component in components.items(): + dependency = {} + if component[KEY_REF]: + dependency["version"] = component[KEY_REF] + if component[KEY_REPO]: + dependency["git"] = component[KEY_REPO] + if component[KEY_PATH]: + dependency["path"] = component[KEY_PATH] + dependencies[name] = dependency + contents = yaml_util.dump({"dependencies": dependencies}) + else: + contents = "" + write_file_if_changed(yml_path, contents) + + # Called by writer.py def copy_files(): if CORE.using_arduino: @@ -936,6 +958,7 @@ def copy_files(): ) if CORE.using_esp_idf: _write_sdkconfig() + _write_idf_component_yml() if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: write_file_if_changed( CORE.relative_build_path("partitions.csv"), @@ -952,55 +975,6 @@ def copy_files(): __version__, ) - import shutil - - shutil.rmtree(CORE.relative_build_path("components"), ignore_errors=True) - - if CORE.data[KEY_ESP32][KEY_COMPONENTS]: - components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] - - for name, component in components.items(): - repo_dir, _ = git.clone_or_update( - url=component[KEY_REPO], - ref=component[KEY_REF], - refresh=component[KEY_REFRESH], - domain="idf_components", - submodules=component[KEY_SUBMODULES], - ) - mkdir_p(CORE.relative_build_path("components")) - component_dir = repo_dir - if component[KEY_PATH] is not None: - component_dir = component_dir / component[KEY_PATH] - - if component[KEY_COMPONENTS] == ["*"]: - shutil.copytree( - component_dir, - CORE.relative_build_path("components"), - dirs_exist_ok=True, - ignore=shutil.ignore_patterns(".git*"), - symlinks=True, - ignore_dangling_symlinks=True, - ) - elif len(component[KEY_COMPONENTS]) > 0: - for comp in component[KEY_COMPONENTS]: - shutil.copytree( - component_dir / comp, - CORE.relative_build_path(f"components/{comp}"), - dirs_exist_ok=True, - ignore=shutil.ignore_patterns(".git*"), - symlinks=True, - ignore_dangling_symlinks=True, - ) - else: - shutil.copytree( - component_dir, - CORE.relative_build_path(f"components/{name}"), - dirs_exist_ok=True, - ignore=shutil.ignore_patterns(".git*"), - symlinks=True, - ignore_dangling_symlinks=True, - ) - for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items(): if file[KEY_PATH].startswith("http"): import requests From f7ac32cedaa604941d1deaaed4025403b17299cf Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 26 Jun 2025 00:35:30 -0500 Subject: [PATCH 126/293] [ld2450] More optimizing, fix copypasta (#9210) --- esphome/components/ld2450/ld2450.cpp | 18 ++++++------------ esphome/components/ld2450/ld2450.h | 9 ++++----- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 718c853d22..e78b79bead 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -17,8 +17,9 @@ namespace esphome { namespace ld2450 { static const char *const TAG = "ld2450"; -static const char *const NO_MAC("08:05:04:03:02:01"); -static const char *const UNKNOWN_MAC("unknown"); +static const char *const NO_MAC = "08:05:04:03:02:01"; +static const char *const UNKNOWN_MAC = "unknown"; +static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; // LD2450 UART Serial Commands static const uint8_t CMD_ENABLE_CONF = 0x00FF; @@ -98,13 +99,6 @@ static inline std::string get_direction(int16_t speed) { return STATIONARY; } -static inline std::string format_version(uint8_t *buffer) { - return str_sprintf("%u.%02X.%02X%02X%02X%02X", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], - buffer[14]); -} - -LD2450Component::LD2450Component() {} - void LD2450Component::setup() { ESP_LOGCONFIG(TAG, "Running setup"); #ifdef USE_NUMBER @@ -189,7 +183,7 @@ void LD2450Component::dump_config() { " Throttle: %ums\n" " MAC Address: %s\n" " Firmware version: %s", - this->throttle_, const_cast(this->mac_.c_str()), const_cast(this->version_.c_str())); + this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str()); } void LD2450Component::loop() { @@ -596,7 +590,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { #endif break; case lowbyte(CMD_VERSION): - this->version_ = ld2450::format_version(buffer); + this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]); ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { @@ -617,7 +611,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { #endif #ifdef USE_SWITCH if (this->bluetooth_switch_ != nullptr) { - this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); + this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC); } #endif break; diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index e0927e5d7d..cd3cb52a62 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -141,7 +141,6 @@ class LD2450Component : public Component, public uart::UARTDevice { #endif public: - LD2450Component(); void setup() override; void dump_config() override; void loop() override; @@ -197,17 +196,17 @@ class LD2450Component : public Component, public uart::UARTDevice { bool get_timeout_status_(uint32_t check_millis); uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving); - Target target_info_[MAX_TARGETS]; - Zone zone_config_[MAX_ZONES]; - uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer - uint8_t buffer_data_[MAX_LINE_LENGTH]; uint32_t last_periodic_millis_ = 0; uint32_t presence_millis_ = 0; uint32_t still_presence_millis_ = 0; uint32_t moving_presence_millis_ = 0; uint16_t throttle_ = 0; uint16_t timeout_ = 5; + uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer + uint8_t buffer_data_[MAX_LINE_LENGTH]; uint8_t zone_type_ = 0; + Target target_info_[MAX_TARGETS]; + Zone zone_config_[MAX_ZONES]; std::string version_{}; std::string mac_{}; #ifdef USE_NUMBER From 95493040071e6666a2fd8b03b0cf6f2bfdb3e4f3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 26 Jun 2025 17:44:02 +1200 Subject: [PATCH 127/293] [ci] Lint lock.yml (#9214) --- .github/workflows/lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 4d7c86deee..8806a89748 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -3,7 +3,7 @@ name: Lock closed issues and PRs on: schedule: - - cron: "30 0 * * *" # Run daily at 00:30 UTC + - cron: "30 0 * * *" # Run daily at 00:30 UTC workflow_dispatch: jobs: From 09e5aa60110143553fc423ae6e5162e7dff7fe8b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 26 Jun 2025 17:59:16 +1200 Subject: [PATCH 128/293] [script] Add exec bit to run-in-env (#9212) --- script/run-in-env.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 script/run-in-env.py diff --git a/script/run-in-env.py b/script/run-in-env.py old mode 100644 new mode 100755 From b12b9b97f4a23704b7f4793352a4bed982d4af8b Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 26 Jun 2025 04:04:38 -0500 Subject: [PATCH 129/293] [ld2410] More optimizations (#9209) Co-authored-by: J. Nick Koston --- esphome/components/ld2410/ld2410.cpp | 210 ++++++++++++++++++++++----- esphome/components/ld2410/ld2410.h | 113 +------------- 2 files changed, 176 insertions(+), 147 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index b3c3649ceb..a34f99ee33 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -10,6 +10,7 @@ #include "esphome/core/application.h" +#define CHECK_BIT(var, pos) (((var) >> (pos)) & 1) #define highbyte(val) (uint8_t)((val) >> 8) #define lowbyte(val) (uint8_t)((val) &0xff) @@ -17,8 +18,162 @@ namespace esphome { namespace ld2410 { static const char *const TAG = "ld2410"; +static const char *const NO_MAC = "08:05:04:03:02:01"; +static const char *const UNKNOWN_MAC = "unknown"; +static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; -LD2410Component::LD2410Component() {} +enum BaudRateStructure : uint8_t { + BAUD_RATE_9600 = 1, + BAUD_RATE_19200 = 2, + BAUD_RATE_38400 = 3, + BAUD_RATE_57600 = 4, + BAUD_RATE_115200 = 5, + BAUD_RATE_230400 = 6, + BAUD_RATE_256000 = 7, + BAUD_RATE_460800 = 8, +}; + +enum DistanceResolutionStructure : uint8_t { + DISTANCE_RESOLUTION_0_2 = 0x01, + DISTANCE_RESOLUTION_0_75 = 0x00, +}; + +enum LightFunctionStructure : uint8_t { + LIGHT_FUNCTION_OFF = 0x00, + LIGHT_FUNCTION_BELOW = 0x01, + LIGHT_FUNCTION_ABOVE = 0x02, +}; + +enum OutPinLevelStructure : uint8_t { + OUT_PIN_LEVEL_LOW = 0x00, + OUT_PIN_LEVEL_HIGH = 0x01, +}; + +enum PeriodicDataStructure : uint8_t { + DATA_TYPES = 6, + TARGET_STATES = 8, + MOVING_TARGET_LOW = 9, + MOVING_TARGET_HIGH = 10, + MOVING_ENERGY = 11, + STILL_TARGET_LOW = 12, + STILL_TARGET_HIGH = 13, + STILL_ENERGY = 14, + DETECT_DISTANCE_LOW = 15, + DETECT_DISTANCE_HIGH = 16, + MOVING_SENSOR_START = 19, + STILL_SENSOR_START = 28, + LIGHT_SENSOR = 37, + OUT_PIN_SENSOR = 38, +}; + +enum PeriodicDataValue : uint8_t { + HEAD = 0xAA, + END = 0x55, + CHECK = 0x00, +}; + +enum AckDataStructure : uint8_t { + COMMAND = 6, + COMMAND_STATUS = 7, +}; + +// Memory-efficient lookup tables +struct StringToUint8 { + const char *str; + uint8_t value; +}; + +struct Uint8ToString { + uint8_t value; + const char *str; +}; + +constexpr StringToUint8 BAUD_RATES_BY_STR[] = { + {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, + {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, + {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}, +}; + +constexpr StringToUint8 DISTANCE_RESOLUTIONS_BY_STR[] = { + {"0.2m", DISTANCE_RESOLUTION_0_2}, + {"0.75m", DISTANCE_RESOLUTION_0_75}, +}; + +constexpr Uint8ToString DISTANCE_RESOLUTIONS_BY_UINT[] = { + {DISTANCE_RESOLUTION_0_2, "0.2m"}, + {DISTANCE_RESOLUTION_0_75, "0.75m"}, +}; + +constexpr StringToUint8 LIGHT_FUNCTIONS_BY_STR[] = { + {"off", LIGHT_FUNCTION_OFF}, + {"below", LIGHT_FUNCTION_BELOW}, + {"above", LIGHT_FUNCTION_ABOVE}, +}; + +constexpr Uint8ToString LIGHT_FUNCTIONS_BY_UINT[] = { + {LIGHT_FUNCTION_OFF, "off"}, + {LIGHT_FUNCTION_BELOW, "below"}, + {LIGHT_FUNCTION_ABOVE, "above"}, +}; + +constexpr StringToUint8 OUT_PIN_LEVELS_BY_STR[] = { + {"low", OUT_PIN_LEVEL_LOW}, + {"high", OUT_PIN_LEVEL_HIGH}, +}; + +constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = { + {OUT_PIN_LEVEL_LOW, "low"}, + {OUT_PIN_LEVEL_HIGH, "high"}, +}; + +// Helper functions for lookups +template uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { + for (const auto &entry : arr) { + if (str == entry.str) + return entry.value; + } + return 0xFF; // Not found +} + +template const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) { + for (const auto &entry : arr) { + if (value == entry.value) + return entry.str; + } + return ""; // Not found +} + +// Commands +static const uint8_t CMD_ENABLE_CONF = 0xFF; +static const uint8_t CMD_DISABLE_CONF = 0xFE; +static const uint8_t CMD_ENABLE_ENG = 0x62; +static const uint8_t CMD_DISABLE_ENG = 0x63; +static const uint8_t CMD_MAXDIST_DURATION = 0x60; +static const uint8_t CMD_QUERY = 0x61; +static const uint8_t CMD_GATE_SENS = 0x64; +static const uint8_t CMD_VERSION = 0xA0; +static const uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0xAB; +static const uint8_t CMD_SET_DISTANCE_RESOLUTION = 0xAA; +static const uint8_t CMD_QUERY_LIGHT_CONTROL = 0xAE; +static const uint8_t CMD_SET_LIGHT_CONTROL = 0xAD; +static const uint8_t CMD_SET_BAUD_RATE = 0xA1; +static const uint8_t CMD_BT_PASSWORD = 0xA9; +static const uint8_t CMD_MAC = 0xA5; +static const uint8_t CMD_RESET = 0xA2; +static const uint8_t CMD_RESTART = 0xA3; +static const uint8_t CMD_BLUETOOTH = 0xA4; +// Commands values +static const uint8_t CMD_MAX_MOVE_VALUE = 0x00; +static const uint8_t CMD_MAX_STILL_VALUE = 0x01; +static const uint8_t CMD_DURATION_VALUE = 0x02; +// Command Header & Footer +static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; +static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; +// Data Header & Footer +static const uint8_t DATA_FRAME_HEADER[4] = {0xF4, 0xF3, 0xF2, 0xF1}; +static const uint8_t DATA_FRAME_END[4] = {0xF8, 0xF7, 0xF6, 0xF5}; + +static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } void LD2410Component::dump_config() { ESP_LOGCONFIG(TAG, "LD2410:"); @@ -78,7 +233,7 @@ void LD2410Component::dump_config() { " Throttle: %ums\n" " MAC address: %s\n" " Firmware version: %s", - this->throttle_, const_cast(this->mac_.c_str()), const_cast(this->version_.c_str())); + this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str()); } void LD2410Component::setup() { @@ -200,7 +355,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { */ #ifdef USE_SENSOR if (this->moving_target_distance_sensor_ != nullptr) { - int new_moving_target_distance = this->two_byte_to_int_(buffer[MOVING_TARGET_LOW], buffer[MOVING_TARGET_HIGH]); + int new_moving_target_distance = ld2410::two_byte_to_int(buffer[MOVING_TARGET_LOW], buffer[MOVING_TARGET_HIGH]); if (this->moving_target_distance_sensor_->get_state() != new_moving_target_distance) this->moving_target_distance_sensor_->publish_state(new_moving_target_distance); } @@ -210,7 +365,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { this->moving_target_energy_sensor_->publish_state(new_moving_target_energy); } if (this->still_target_distance_sensor_ != nullptr) { - int new_still_target_distance = this->two_byte_to_int_(buffer[STILL_TARGET_LOW], buffer[STILL_TARGET_HIGH]); + int new_still_target_distance = ld2410::two_byte_to_int(buffer[STILL_TARGET_LOW], buffer[STILL_TARGET_HIGH]); if (this->still_target_distance_sensor_->get_state() != new_still_target_distance) this->still_target_distance_sensor_->publish_state(new_still_target_distance); } @@ -220,7 +375,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { this->still_target_energy_sensor_->publish_state(new_still_target_energy); } if (this->detection_distance_sensor_ != nullptr) { - int new_detect_distance = this->two_byte_to_int_(buffer[DETECT_DISTANCE_LOW], buffer[DETECT_DISTANCE_HIGH]); + int new_detect_distance = ld2410::two_byte_to_int(buffer[DETECT_DISTANCE_LOW], buffer[DETECT_DISTANCE_HIGH]); if (this->detection_distance_sensor_->get_state() != new_detect_distance) this->detection_distance_sensor_->publish_state(new_detect_distance); } @@ -282,25 +437,6 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { #endif } -const char VERSION_FMT[] = "%u.%02X.%02X%02X%02X%02X"; - -std::string format_version(uint8_t *buffer) { - std::string::size_type version_size = 256; - std::string version; - do { - version.resize(version_size + 1); - version_size = std::snprintf(&version[0], version.size(), VERSION_FMT, buffer[13], buffer[12], buffer[17], - buffer[16], buffer[15], buffer[14]); - } while (version_size + 1 > version.size()); - version.resize(version_size); - return version; -} - -const char MAC_FMT[] = "%02X:%02X:%02X:%02X:%02X:%02X"; - -const std::string UNKNOWN_MAC("unknown"); -const std::string NO_MAC("08:05:04:03:02:01"); - #ifdef USE_NUMBER std::function set_number_value(number::Number *n, float value) { float normalized_value = value * 1.0; @@ -326,7 +462,7 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { ESP_LOGE(TAG, "Invalid status"); return true; } - if (this->two_byte_to_int_(buffer[8], buffer[9]) != 0x00) { + if (ld2410::two_byte_to_int(buffer[8], buffer[9]) != 0x00) { ESP_LOGE(TAG, "Invalid command: %u, %u", buffer[8], buffer[9]); return true; } @@ -347,8 +483,8 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { #endif break; case lowbyte(CMD_VERSION): - this->version_ = format_version(buffer); - ESP_LOGV(TAG, "Firmware version: %s", const_cast(this->version_.c_str())); + this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]); + ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { this->version_text_sensor_->publish_state(this->version_); @@ -357,8 +493,8 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { break; case lowbyte(CMD_QUERY_DISTANCE_RESOLUTION): { std::string distance_resolution = - DISTANCE_RESOLUTION_INT_TO_ENUM.at(this->two_byte_to_int_(buffer[10], buffer[11])); - ESP_LOGV(TAG, "Distance resolution: %s", const_cast(distance_resolution.c_str())); + find_str(DISTANCE_RESOLUTIONS_BY_UINT, ld2410::two_byte_to_int(buffer[10], buffer[11])); + ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution.c_str()); #ifdef USE_SELECT if (this->distance_resolution_select_ != nullptr && this->distance_resolution_select_->state != distance_resolution) { @@ -367,9 +503,9 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { #endif } break; case lowbyte(CMD_QUERY_LIGHT_CONTROL): { - this->light_function_ = LIGHT_FUNCTION_INT_TO_ENUM.at(buffer[10]); + this->light_function_ = find_str(LIGHT_FUNCTIONS_BY_UINT, buffer[10]); this->light_threshold_ = buffer[11] * 1.0; - this->out_pin_level_ = OUT_PIN_LEVEL_INT_TO_ENUM.at(buffer[12]); + this->out_pin_level_ = find_str(OUT_PIN_LEVELS_BY_UINT, buffer[12]); ESP_LOGV(TAG, "Light function: %s", const_cast(this->light_function_.c_str())); ESP_LOGV(TAG, "Light threshold: %f", this->light_threshold_); ESP_LOGV(TAG, "Out pin level: %s", const_cast(this->out_pin_level_.c_str())); @@ -402,7 +538,7 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { #endif #ifdef USE_SWITCH if (this->bluetooth_switch_ != nullptr) { - this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); + this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC); } #endif break; @@ -448,7 +584,7 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { /* None Duration: 33~34th bytes */ - updates.push_back(set_number_value(this->timeout_number_, this->two_byte_to_int_(buffer[32], buffer[33]))); + updates.push_back(set_number_value(this->timeout_number_, ld2410::two_byte_to_int(buffer[32], buffer[33]))); for (auto &update : updates) { update(); } @@ -505,14 +641,14 @@ void LD2410Component::set_bluetooth(bool enable) { void LD2410Component::set_distance_resolution(const std::string &state) { this->set_config_mode_(true); - uint8_t cmd_value[2] = {DISTANCE_RESOLUTION_ENUM_TO_INT.at(state), 0x00}; + uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00}; this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, 2); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); } void LD2410Component::set_baud_rate(const std::string &state) { this->set_config_mode_(true); - uint8_t cmd_value[2] = {BAUD_RATE_ENUM_TO_INT.at(state), 0x00}; + uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); this->set_timeout(200, [this]() { this->restart_(); }); } @@ -646,9 +782,9 @@ void LD2410Component::set_light_out_control() { return; } this->set_config_mode_(true); - uint8_t light_function = LIGHT_FUNCTION_ENUM_TO_INT.at(this->light_function_); + uint8_t light_function = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_); uint8_t light_threshold = static_cast(this->light_threshold_); - uint8_t out_pin_level = OUT_PIN_LEVEL_ENUM_TO_INT.at(this->out_pin_level_); + uint8_t out_pin_level = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_); uint8_t value[4] = {light_function, light_threshold, out_pin_level, 0x00}; this->send_command_(CMD_SET_LIGHT_CONTROL, value, 4); delay(50); // NOLINT diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index 1bbaa8987a..1b5f6e3057 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -26,114 +26,9 @@ #include "esphome/core/automation.h" #include "esphome/core/helpers.h" -#include - namespace esphome { namespace ld2410 { -#define CHECK_BIT(var, pos) (((var) >> (pos)) & 1) - -// Commands -static const uint8_t CMD_ENABLE_CONF = 0x00FF; -static const uint8_t CMD_DISABLE_CONF = 0x00FE; -static const uint8_t CMD_ENABLE_ENG = 0x0062; -static const uint8_t CMD_DISABLE_ENG = 0x0063; -static const uint8_t CMD_MAXDIST_DURATION = 0x0060; -static const uint8_t CMD_QUERY = 0x0061; -static const uint8_t CMD_GATE_SENS = 0x0064; -static const uint8_t CMD_VERSION = 0x00A0; -static const uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0x00AB; -static const uint8_t CMD_SET_DISTANCE_RESOLUTION = 0x00AA; -static const uint8_t CMD_QUERY_LIGHT_CONTROL = 0x00AE; -static const uint8_t CMD_SET_LIGHT_CONTROL = 0x00AD; -static const uint8_t CMD_SET_BAUD_RATE = 0x00A1; -static const uint8_t CMD_BT_PASSWORD = 0x00A9; -static const uint8_t CMD_MAC = 0x00A5; -static const uint8_t CMD_RESET = 0x00A2; -static const uint8_t CMD_RESTART = 0x00A3; -static const uint8_t CMD_BLUETOOTH = 0x00A4; - -enum BaudRateStructure : uint8_t { - BAUD_RATE_9600 = 1, - BAUD_RATE_19200 = 2, - BAUD_RATE_38400 = 3, - BAUD_RATE_57600 = 4, - BAUD_RATE_115200 = 5, - BAUD_RATE_230400 = 6, - BAUD_RATE_256000 = 7, - BAUD_RATE_460800 = 8 -}; - -static const std::map BAUD_RATE_ENUM_TO_INT{ - {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, - {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, - {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}}; - -enum DistanceResolutionStructure : uint8_t { DISTANCE_RESOLUTION_0_2 = 0x01, DISTANCE_RESOLUTION_0_75 = 0x00 }; - -static const std::map DISTANCE_RESOLUTION_ENUM_TO_INT{{"0.2m", DISTANCE_RESOLUTION_0_2}, - {"0.75m", DISTANCE_RESOLUTION_0_75}}; -static const std::map DISTANCE_RESOLUTION_INT_TO_ENUM{{DISTANCE_RESOLUTION_0_2, "0.2m"}, - {DISTANCE_RESOLUTION_0_75, "0.75m"}}; - -enum LightFunctionStructure : uint8_t { - LIGHT_FUNCTION_OFF = 0x00, - LIGHT_FUNCTION_BELOW = 0x01, - LIGHT_FUNCTION_ABOVE = 0x02 -}; - -static const std::map LIGHT_FUNCTION_ENUM_TO_INT{ - {"off", LIGHT_FUNCTION_OFF}, {"below", LIGHT_FUNCTION_BELOW}, {"above", LIGHT_FUNCTION_ABOVE}}; -static const std::map LIGHT_FUNCTION_INT_TO_ENUM{ - {LIGHT_FUNCTION_OFF, "off"}, {LIGHT_FUNCTION_BELOW, "below"}, {LIGHT_FUNCTION_ABOVE, "above"}}; - -enum OutPinLevelStructure : uint8_t { OUT_PIN_LEVEL_LOW = 0x00, OUT_PIN_LEVEL_HIGH = 0x01 }; - -static const std::map OUT_PIN_LEVEL_ENUM_TO_INT{{"low", OUT_PIN_LEVEL_LOW}, - {"high", OUT_PIN_LEVEL_HIGH}}; -static const std::map OUT_PIN_LEVEL_INT_TO_ENUM{{OUT_PIN_LEVEL_LOW, "low"}, - {OUT_PIN_LEVEL_HIGH, "high"}}; - -// Commands values -static const uint8_t CMD_MAX_MOVE_VALUE = 0x0000; -static const uint8_t CMD_MAX_STILL_VALUE = 0x0001; -static const uint8_t CMD_DURATION_VALUE = 0x0002; -// Command Header & Footer -static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; -static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; -// Data Header & Footer -static const uint8_t DATA_FRAME_HEADER[4] = {0xF4, 0xF3, 0xF2, 0xF1}; -static const uint8_t DATA_FRAME_END[4] = {0xF8, 0xF7, 0xF6, 0xF5}; -/* -Data Type: 6th byte -Target states: 9th byte - Moving target distance: 10~11th bytes - Moving target energy: 12th byte - Still target distance: 13~14th bytes - Still target energy: 15th byte - Detect distance: 16~17th bytes -*/ -enum PeriodicDataStructure : uint8_t { - DATA_TYPES = 6, - TARGET_STATES = 8, - MOVING_TARGET_LOW = 9, - MOVING_TARGET_HIGH = 10, - MOVING_ENERGY = 11, - STILL_TARGET_LOW = 12, - STILL_TARGET_HIGH = 13, - STILL_ENERGY = 14, - DETECT_DISTANCE_LOW = 15, - DETECT_DISTANCE_HIGH = 16, - MOVING_SENSOR_START = 19, - STILL_SENSOR_START = 28, - LIGHT_SENSOR = 37, - OUT_PIN_SENSOR = 38, -}; -enum PeriodicDataValue : uint8_t { HEAD = 0xAA, END = 0x55, CHECK = 0x00 }; - -enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 }; - -// char cmd[2] = {enable ? 0xFF : 0xFE, 0x00}; class LD2410Component : public Component, public uart::UARTDevice { #ifdef USE_SENSOR SUB_SENSOR(moving_target_distance) @@ -176,7 +71,6 @@ class LD2410Component : public Component, public uart::UARTDevice { #endif public: - LD2410Component(); void setup() override; void dump_config() override; void loop() override; @@ -202,7 +96,6 @@ class LD2410Component : public Component, public uart::UARTDevice { void factory_reset(); protected: - int two_byte_to_int_(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } void send_command_(uint8_t command_str, const uint8_t *command_value, int command_value_len); void set_config_mode_(bool enable); void handle_periodic_data_(uint8_t *buffer, int len); @@ -215,14 +108,14 @@ class LD2410Component : public Component, public uart::UARTDevice { void get_light_control_(); void restart_(); - int32_t last_periodic_millis_ = millis(); - int32_t last_engineering_mode_change_millis_ = millis(); + int32_t last_periodic_millis_ = 0; + int32_t last_engineering_mode_change_millis_ = 0; uint16_t throttle_; + float light_threshold_ = -1; std::string version_; std::string mac_; std::string out_pin_level_; std::string light_function_; - float light_threshold_ = -1; #ifdef USE_NUMBER std::vector gate_still_threshold_numbers_ = std::vector(9); std::vector gate_move_threshold_numbers_ = std::vector(9); From 2930c8e9a8a12d87afc0ebd67041dad90eaf4f04 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 26 Jun 2025 04:37:27 -0500 Subject: [PATCH 130/293] [ld2450] Move consts to cpp file, optimize memory use (#9215) --- esphome/components/ld2450/ld2450.cpp | 117 +++++++++++++++++++++++---- esphome/components/ld2450/ld2450.h | 45 ----------- 2 files changed, 101 insertions(+), 61 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index e78b79bead..0e1123db1a 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -21,20 +21,105 @@ static const char *const NO_MAC = "08:05:04:03:02:01"; static const char *const UNKNOWN_MAC = "unknown"; static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; +enum BaudRateStructure : uint8_t { + BAUD_RATE_9600 = 1, + BAUD_RATE_19200 = 2, + BAUD_RATE_38400 = 3, + BAUD_RATE_57600 = 4, + BAUD_RATE_115200 = 5, + BAUD_RATE_230400 = 6, + BAUD_RATE_256000 = 7, + BAUD_RATE_460800 = 8 +}; + +// Zone type struct +enum ZoneTypeStructure : uint8_t { + ZONE_DISABLED = 0, + ZONE_DETECTION = 1, + ZONE_FILTER = 2, +}; + +enum PeriodicDataStructure : uint8_t { + TARGET_X = 4, + TARGET_Y = 6, + TARGET_SPEED = 8, + TARGET_RESOLUTION = 10, +}; + +enum PeriodicDataValue : uint8_t { + HEAD = 0xAA, + END = 0x55, + CHECK = 0x00, +}; + +enum AckDataStructure : uint8_t { + COMMAND = 6, + COMMAND_STATUS = 7, +}; + +// Memory-efficient lookup tables +struct StringToUint8 { + const char *str; + uint8_t value; +}; + +struct Uint8ToString { + uint8_t value; + const char *str; +}; + +constexpr StringToUint8 BAUD_RATES_BY_STR[] = { + {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, + {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, + {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}, +}; + +constexpr Uint8ToString ZONE_TYPE_BY_UINT[] = { + {ZONE_DISABLED, "Disabled"}, + {ZONE_DETECTION, "Detection"}, + {ZONE_FILTER, "Filter"}, +}; + +constexpr StringToUint8 ZONE_TYPE_BY_STR[] = { + {"Disabled", ZONE_DISABLED}, + {"Detection", ZONE_DETECTION}, + {"Filter", ZONE_FILTER}, +}; + +// Helper functions for lookups +template uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { + for (const auto &entry : arr) { + if (str == entry.str) + return entry.value; + } + return 0xFF; // Not found +} + +template const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) { + for (const auto &entry : arr) { + if (value == entry.value) + return entry.str; + } + return ""; // Not found +} + +// LD2450 serial command header & footer +static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; +static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; // LD2450 UART Serial Commands -static const uint8_t CMD_ENABLE_CONF = 0x00FF; -static const uint8_t CMD_DISABLE_CONF = 0x00FE; -static const uint8_t CMD_VERSION = 0x00A0; -static const uint8_t CMD_MAC = 0x00A5; -static const uint8_t CMD_RESET = 0x00A2; -static const uint8_t CMD_RESTART = 0x00A3; -static const uint8_t CMD_BLUETOOTH = 0x00A4; -static const uint8_t CMD_SINGLE_TARGET_MODE = 0x0080; -static const uint8_t CMD_MULTI_TARGET_MODE = 0x0090; -static const uint8_t CMD_QUERY_TARGET_MODE = 0x0091; -static const uint8_t CMD_SET_BAUD_RATE = 0x00A1; -static const uint8_t CMD_QUERY_ZONE = 0x00C1; -static const uint8_t CMD_SET_ZONE = 0x00C2; +static const uint8_t CMD_ENABLE_CONF = 0xFF; +static const uint8_t CMD_DISABLE_CONF = 0xFE; +static const uint8_t CMD_VERSION = 0xA0; +static const uint8_t CMD_MAC = 0xA5; +static const uint8_t CMD_RESET = 0xA2; +static const uint8_t CMD_RESTART = 0xA3; +static const uint8_t CMD_BLUETOOTH = 0xA4; +static const uint8_t CMD_SINGLE_TARGET_MODE = 0x80; +static const uint8_t CMD_MULTI_TARGET_MODE = 0x90; +static const uint8_t CMD_QUERY_TARGET_MODE = 0x91; +static const uint8_t CMD_SET_BAUD_RATE = 0xA1; +static const uint8_t CMD_QUERY_ZONE = 0xC1; +static const uint8_t CMD_SET_ZONE = 0xC2; static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; }; @@ -720,7 +805,7 @@ void LD2450Component::set_bluetooth(bool enable) { // Set Baud rate void LD2450Component::set_baud_rate(const std::string &state) { this->set_config_mode_(true); - uint8_t cmd_value[2] = {BAUD_RATE_ENUM_TO_INT.at(state), 0x00}; + uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); this->set_timeout(200, [this]() { this->restart_(); }); } @@ -728,7 +813,7 @@ void LD2450Component::set_baud_rate(const std::string &state) { // Set Zone Type - one of: Disabled, Detection, Filter void LD2450Component::set_zone_type(const std::string &state) { ESP_LOGV(TAG, "Set zone type: %s", state.c_str()); - uint8_t zone_type = ZONE_TYPE_ENUM_TO_INT.at(state); + uint8_t zone_type = find_uint8(ZONE_TYPE_BY_STR, state); this->zone_type_ = zone_type; this->send_set_zone_command_(); } @@ -736,7 +821,7 @@ void LD2450Component::set_zone_type(const std::string &state) { // Publish Zone Type to Select component void LD2450Component::publish_zone_type() { #ifdef USE_SELECT - std::string zone_type = ZONE_TYPE_INT_TO_ENUM.at(static_cast(this->zone_type_)); + std::string zone_type = find_str(ZONE_TYPE_BY_UINT, this->zone_type_); if (this->zone_type_select_ != nullptr) { this->zone_type_select_->publish_state(zone_type); } diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index cd3cb52a62..b0c19dc96c 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -1,7 +1,5 @@ #pragma once -#include -#include #include "esphome/components/uart/uart.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" @@ -66,49 +64,6 @@ struct ZoneOfNumbers { }; #endif -enum BaudRateStructure : uint8_t { - BAUD_RATE_9600 = 1, - BAUD_RATE_19200 = 2, - BAUD_RATE_38400 = 3, - BAUD_RATE_57600 = 4, - BAUD_RATE_115200 = 5, - BAUD_RATE_230400 = 6, - BAUD_RATE_256000 = 7, - BAUD_RATE_460800 = 8 -}; - -// Convert baud rate enum to int -static const std::map BAUD_RATE_ENUM_TO_INT{ - {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, - {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, - {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}}; - -// Zone type struct -enum ZoneTypeStructure : uint8_t { ZONE_DISABLED = 0, ZONE_DETECTION = 1, ZONE_FILTER = 2 }; - -// Convert zone type int to enum -static const std::map ZONE_TYPE_INT_TO_ENUM{ - {ZONE_DISABLED, "Disabled"}, {ZONE_DETECTION, "Detection"}, {ZONE_FILTER, "Filter"}}; - -// Convert zone type enum to int -static const std::map ZONE_TYPE_ENUM_TO_INT{ - {"Disabled", ZONE_DISABLED}, {"Detection", ZONE_DETECTION}, {"Filter", ZONE_FILTER}}; - -// LD2450 serial command header & footer -static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; -static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; - -enum PeriodicDataStructure : uint8_t { - TARGET_X = 4, - TARGET_Y = 6, - TARGET_SPEED = 8, - TARGET_RESOLUTION = 10, -}; - -enum PeriodicDataValue : uint8_t { HEAD = 0xAA, END = 0x55, CHECK = 0x00 }; - -enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 }; - class LD2450Component : public Component, public uart::UARTDevice { #ifdef USE_SENSOR SUB_SENSOR(target_count) From 15c5dd222fcfcf4236e248cef3bfc0b29ad8bfc3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 26 Jun 2025 23:21:19 +1200 Subject: [PATCH 131/293] [tests] Remove extra newline (#9213) --- .../build_components_base.esp32-p4-idf.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_build_components/build_components_base.esp32-p4-idf.yaml b/tests/test_build_components/build_components_base.esp32-p4-idf.yaml index e2b975f643..9e4f0ddd61 100644 --- a/tests/test_build_components/build_components_base.esp32-p4-idf.yaml +++ b/tests/test_build_components/build_components_base.esp32-p4-idf.yaml @@ -15,4 +15,3 @@ packages: file: $component_test_file vars: component_test_file: $component_test_file - From d94896c0fbfd5428ecd870380ad17988f773d0b4 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 26 Jun 2025 20:11:50 +0100 Subject: [PATCH 132/293] [audio] Bugfix: improve timeout handling (#9221) --- esphome/components/audio/audio_reader.cpp | 62 +++++++++++++++-------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/esphome/components/audio/audio_reader.cpp b/esphome/components/audio/audio_reader.cpp index b82c6db9ee..6966c95db7 100644 --- a/esphome/components/audio/audio_reader.cpp +++ b/esphome/components/audio/audio_reader.cpp @@ -5,6 +5,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE #include "esp_crt_bundle.h" @@ -16,13 +17,13 @@ namespace audio { static const uint32_t READ_WRITE_TIMEOUT_MS = 20; static const uint32_t CONNECTION_TIMEOUT_MS = 5000; - -// The number of times the http read times out with no data before throwing an error -static const uint32_t ERROR_COUNT_NO_DATA_READ_TIMEOUT = 100; +static const uint8_t MAX_FETCHING_HEADER_ATTEMPTS = 6; static const size_t HTTP_STREAM_BUFFER_SIZE = 2048; -static const uint8_t MAX_REDIRECTION = 5; +static const uint8_t MAX_REDIRECTIONS = 5; + +static const char *const TAG = "audio_reader"; // Some common HTTP status codes - borrowed from http_request component accessed 20241224 enum HttpStatus { @@ -94,7 +95,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { client_config.url = uri.c_str(); client_config.cert_pem = nullptr; client_config.disable_auto_redirect = false; - client_config.max_redirection_count = 10; + client_config.max_redirection_count = MAX_REDIRECTIONS; client_config.event_handler = http_event_handler; client_config.user_data = this; client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE; @@ -116,12 +117,29 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { esp_err_t err = esp_http_client_open(this->client_, 0); if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to open URL"); this->cleanup_connection_(); return err; } int64_t header_length = esp_http_client_fetch_headers(this->client_); + uint8_t reattempt_count = 0; + while ((header_length < 0) && (reattempt_count < MAX_FETCHING_HEADER_ATTEMPTS)) { + this->cleanup_connection_(); + if (header_length != -ESP_ERR_HTTP_EAGAIN) { + // Serious error, no recovery + return ESP_FAIL; + } else { + // Reconnect from a fresh state to avoid a bug where it never reads the headers even if made available + this->client_ = esp_http_client_init(&client_config); + esp_http_client_open(this->client_, 0); + header_length = esp_http_client_fetch_headers(this->client_); + ++reattempt_count; + } + } + if (header_length < 0) { + ESP_LOGE(TAG, "Failed to fetch headers"); this->cleanup_connection_(); return ESP_FAIL; } @@ -135,7 +153,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { ssize_t redirect_count = 0; - while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTION)) { + while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTIONS)) { err = esp_http_client_open(this->client_, 0); if (err != ESP_OK) { this->cleanup_connection_(); @@ -267,27 +285,29 @@ AudioReaderState AudioReader::http_read_() { return AudioReaderState::FINISHED; } } else if (this->output_transfer_buffer_->free() > 0) { - size_t bytes_to_read = this->output_transfer_buffer_->free(); - int received_len = - esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), bytes_to_read); + int received_len = esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), + this->output_transfer_buffer_->free()); if (received_len > 0) { this->output_transfer_buffer_->increase_buffer_length(received_len); this->last_data_read_ms_ = millis(); - } else if (received_len < 0) { + return AudioReaderState::READING; + } else if (received_len <= 0) { // HTTP read error - this->cleanup_connection_(); - return AudioReaderState::FAILED; - } else { - if (bytes_to_read > 0) { - // Read timed out - if ((millis() - this->last_data_read_ms_) > CONNECTION_TIMEOUT_MS) { - this->cleanup_connection_(); - return AudioReaderState::FAILED; - } - - delay(READ_WRITE_TIMEOUT_MS); + if (received_len == -1) { + // A true connection error occured, no chance at recovery + this->cleanup_connection_(); + return AudioReaderState::FAILED; } + + // Read timed out, manually verify if it has been too long since the last successful read + if ((millis() - this->last_data_read_ms_) > MAX_FETCHING_HEADER_ATTEMPTS * CONNECTION_TIMEOUT_MS) { + ESP_LOGE(TAG, "Timed out"); + this->cleanup_connection_(); + return AudioReaderState::FAILED; + } + + delay(READ_WRITE_TIMEOUT_MS); } } From 4fac8e9cd589a1172663c5aa5cd41f0bf77b25eb Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 26 Jun 2025 20:12:58 +0100 Subject: [PATCH 133/293] [speaker] bugfix: continue to block tasks if stop flag is set (#9222) --- .../speaker/media_player/audio_pipeline.cpp | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index ac122b6e0c..333a076bec 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -343,13 +343,12 @@ void AudioPipeline::read_task(void *params) { xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED); // Wait until the pipeline notifies us the source of the media file - EventBits_t event_bits = - xEventGroupWaitBits(this_pipeline->event_group_, - EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP | - EventGroupBits::PIPELINE_COMMAND_STOP, // Bit message to read - pdFALSE, // Clear the bit on exit - pdFALSE, // Wait for all the bits, - portMAX_DELAY); // Block indefinitely until bit is set + EventBits_t event_bits = xEventGroupWaitBits( + this_pipeline->event_group_, + EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP, // Bit message to read + pdFALSE, // Clear the bit on exit + pdFALSE, // Wait for all the bits, + portMAX_DELAY); // Block indefinitely until bit is set if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) { xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED | @@ -434,12 +433,12 @@ void AudioPipeline::decode_task(void *params) { xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED); // Wait until the reader notifies us that the media type is available - EventBits_t event_bits = xEventGroupWaitBits(this_pipeline->event_group_, - EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE | - EventGroupBits::PIPELINE_COMMAND_STOP, // Bit message to read - pdFALSE, // Clear the bit on exit - pdFALSE, // Wait for all the bits, - portMAX_DELAY); // Block indefinitely until bit is set + EventBits_t event_bits = + xEventGroupWaitBits(this_pipeline->event_group_, + EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE, // Bit message to read + pdFALSE, // Clear the bit on exit + pdFALSE, // Wait for all the bits, + portMAX_DELAY); // Block indefinitely until bit is set xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED | EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE); From b182f2d544c1b20f04579d380533ced0fc646983 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 26 Jun 2025 20:18:51 +0100 Subject: [PATCH 134/293] [voice_assistant] Support streaming TTS responses and fixes crash for long responses (#9224) --- CODEOWNERS | 2 +- .../components/voice_assistant/__init__.py | 13 ++++++- .../voice_assistant/voice_assistant.cpp | 38 ++++++++++++++++++- .../voice_assistant/voice_assistant.h | 4 ++ 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 83d64a8850..4eb4c42a6d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -491,7 +491,7 @@ esphome/components/vbus/* @ssieb esphome/components/veml3235/* @kbx81 esphome/components/veml7700/* @latonita esphome/components/version/* @esphome/core -esphome/components/voice_assistant/* @jesserockz +esphome/components/voice_assistant/* @jesserockz @kahrendt esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 esphome/components/watchdog/* @oarcher esphome/components/waveshare_epaper/* @clydebarrow diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index b9309ab422..59c7ec8383 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -17,10 +17,11 @@ from esphome.const import ( AUTO_LOAD = ["socket"] DEPENDENCIES = ["api", "microphone"] -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@kahrendt"] CONF_ON_END = "on_end" CONF_ON_INTENT_END = "on_intent_end" +CONF_ON_INTENT_PROGRESS = "on_intent_progress" CONF_ON_INTENT_START = "on_intent_start" CONF_ON_LISTENING = "on_listening" CONF_ON_START = "on_start" @@ -136,6 +137,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_INTENT_START): automation.validate_automation( single=True ), + cv.Optional(CONF_ON_INTENT_PROGRESS): automation.validate_automation( + single=True + ), cv.Optional(CONF_ON_INTENT_END): automation.validate_automation( single=True ), @@ -282,6 +286,13 @@ async def to_code(config): config[CONF_ON_INTENT_START], ) + if CONF_ON_INTENT_PROGRESS in config: + await automation.build_automation( + var.get_intent_progress_trigger(), + [(cg.std_string, "x")], + config[CONF_ON_INTENT_PROGRESS], + ) + if CONF_ON_INTENT_END in config: await automation.build_automation( var.get_intent_end_trigger(), diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 366a020d1c..9cf7d10936 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -555,7 +555,7 @@ void VoiceAssistant::request_stop() { break; case State::AWAITING_RESPONSE: this->signal_stop_(); - break; + // Fallthrough intended to stop a streaming TTS announcement that has potentially started case State::STREAMING_RESPONSE: #ifdef USE_MEDIA_PLAYER // Stop any ongoing media player announcement @@ -599,6 +599,14 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { switch (msg.event_type) { case api::enums::VOICE_ASSISTANT_RUN_START: ESP_LOGD(TAG, "Assist Pipeline running"); +#ifdef USE_MEDIA_PLAYER + this->started_streaming_tts_ = false; + for (auto arg : msg.data) { + if (arg.name == "url") { + this->tts_response_url_ = std::move(arg.value); + } + } +#endif this->defer([this]() { this->start_trigger_->trigger(); }); break; case api::enums::VOICE_ASSISTANT_WAKE_WORD_START: @@ -622,6 +630,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { if (text.empty()) { ESP_LOGW(TAG, "No text in STT_END event"); return; + } else if (text.length() > 500) { + text = text.substr(0, 497) + "..."; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); this->defer([this, text]() { this->stt_end_trigger_->trigger(text); }); @@ -631,6 +641,27 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGD(TAG, "Intent started"); this->defer([this]() { this->intent_start_trigger_->trigger(); }); break; + case api::enums::VOICE_ASSISTANT_INTENT_PROGRESS: { + ESP_LOGD(TAG, "Intent progress"); + std::string tts_url_for_trigger = ""; +#ifdef USE_MEDIA_PLAYER + if (this->media_player_ != nullptr) { + for (const auto &arg : msg.data) { + if ((arg.name == "tts_start_streaming") && (arg.value == "1") && !this->tts_response_url_.empty()) { + this->media_player_->make_call().set_media_url(this->tts_response_url_).set_announcement(true).perform(); + + this->media_player_wait_for_announcement_start_ = true; + this->media_player_wait_for_announcement_end_ = false; + this->started_streaming_tts_ = true; + tts_url_for_trigger = this->tts_response_url_; + this->tts_response_url_.clear(); // Reset streaming URL + } + } + } +#endif + this->defer([this, tts_url_for_trigger]() { this->intent_progress_trigger_->trigger(tts_url_for_trigger); }); + break; + } case api::enums::VOICE_ASSISTANT_INTENT_END: { for (auto arg : msg.data) { if (arg.name == "conversation_id") { @@ -653,6 +684,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGW(TAG, "No text in TTS_START event"); return; } + if (text.length() > 500) { + text = text.substr(0, 497) + "..."; + } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); this->defer([this, text]() { this->tts_start_trigger_->trigger(text); @@ -678,7 +712,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); this->defer([this, url]() { #ifdef USE_MEDIA_PLAYER - if (this->media_player_ != nullptr) { + if ((this->media_player_ != nullptr) && (!this->started_streaming_tts_)) { this->media_player_->make_call().set_media_url(url).set_announcement(true).perform(); this->media_player_wait_for_announcement_start_ = true; diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 865731522f..2424ea6052 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -177,6 +177,7 @@ class VoiceAssistant : public Component { Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; } Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; } + Trigger *get_intent_progress_trigger() const { return this->intent_progress_trigger_; } Trigger<> *get_listening_trigger() const { return this->listening_trigger_; } Trigger<> *get_end_trigger() const { return this->end_trigger_; } Trigger<> *get_start_trigger() const { return this->start_trigger_; } @@ -233,6 +234,7 @@ class VoiceAssistant : public Component { Trigger<> *tts_stream_start_trigger_ = new Trigger<>(); Trigger<> *tts_stream_end_trigger_ = new Trigger<>(); #endif + Trigger *intent_progress_trigger_ = new Trigger(); Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); Trigger *stt_end_trigger_ = new Trigger(); Trigger *tts_end_trigger_ = new Trigger(); @@ -268,6 +270,8 @@ class VoiceAssistant : public Component { #endif #ifdef USE_MEDIA_PLAYER media_player::MediaPlayer *media_player_{nullptr}; + std::string tts_response_url_{""}; + bool started_streaming_tts_{false}; bool media_player_wait_for_announcement_start_{false}; bool media_player_wait_for_announcement_end_{false}; #endif From 4f5aacdb3a113819294e2fcb8c9700d6dc44a12d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 01:25:26 +0200 Subject: [PATCH 135/293] Optimize SafeModeComponent memory layout to reduce padding (#9228) --- esphome/components/safe_mode/safe_mode.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index 37e2c3a3d6..028b7b11cb 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -33,12 +33,15 @@ class SafeModeComponent : public Component { void write_rtc_(uint32_t val); uint32_t read_rtc_(); - bool boot_successful_{false}; ///< set to true after boot is considered successful + // Group all 4-byte aligned members together to avoid padding uint32_t safe_mode_boot_is_good_after_{60000}; ///< The amount of time after which the boot is considered successful uint32_t safe_mode_enable_time_{60000}; ///< The time safe mode should remain active for uint32_t safe_mode_rtc_value_{0}; uint32_t safe_mode_start_time_{0}; ///< stores when safe mode was enabled + // Group 1-byte members together to minimize padding + bool boot_successful_{false}; ///< set to true after boot is considered successful uint8_t safe_mode_num_attempts_{0}; + // Larger objects at the end ESPPreferenceObject rtc_; CallbackManager safe_mode_callback_{}; From 87321ce10b8611a5f8ea14787e497dcc8a278051 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 26 Jun 2025 19:51:13 -0400 Subject: [PATCH 136/293] [esp32_hosted] Add support for remote wifi (#8833) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/esp32_hosted/__init__.py | 101 ++++++++++++++++++ .../esp32_hosted/esp32_hosted.py.script | 12 +++ esphome/idf_component.yml | 12 +++ tests/components/esp32_hosted/common.yaml | 15 +++ .../esp32_hosted/test.esp32-p4-idf.yaml | 1 + 6 files changed, 142 insertions(+) create mode 100644 esphome/components/esp32_hosted/__init__.py create mode 100644 esphome/components/esp32_hosted/esp32_hosted.py.script create mode 100644 tests/components/esp32_hosted/common.yaml create mode 100644 tests/components/esp32_hosted/test.esp32-p4-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 4eb4c42a6d..832c571ae4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -146,6 +146,7 @@ esphome/components/esp32_ble_client/* @jesserockz esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_can/* @Sympatron +esphome/components/esp32_hosted/* @swoboda1337 esphome/components/esp32_improv/* @jesserockz esphome/components/esp32_rmt/* @jesserockz esphome/components/esp32_rmt_led_strip/* @jesserockz diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py new file mode 100644 index 0000000000..330800df12 --- /dev/null +++ b/esphome/components/esp32_hosted/__init__.py @@ -0,0 +1,101 @@ +import os + +from esphome import pins +from esphome.components import esp32 +import esphome.config_validation as cv +from esphome.const import ( + CONF_CLK_PIN, + CONF_RESET_PIN, + CONF_VARIANT, + KEY_CORE, + KEY_FRAMEWORK_VERSION, +) +from esphome.core import CORE + +CODEOWNERS = ["@swoboda1337"] + +CONF_ACTIVE_HIGH = "active_high" +CONF_CMD_PIN = "cmd_pin" +CONF_D0_PIN = "d0_pin" +CONF_D1_PIN = "d1_pin" +CONF_D2_PIN = "d2_pin" +CONF_D3_PIN = "d3_pin" +CONF_SLOT = "slot" + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_VARIANT): cv.one_of(*esp32.VARIANTS, upper=True), + cv.Required(CONF_ACTIVE_HIGH): cv.boolean, + cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_CMD_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_D0_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_D1_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_D2_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_D3_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1), + } + ), +) + + +async def to_code(config): + if config[CONF_ACTIVE_HIGH]: + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH", + True, + ) + else: + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_LOW", + True, + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE", # NOLINT + config[CONF_RESET_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_SLAVE_IDF_TARGET_{config[CONF_VARIANT]}", # NOLINT + True, + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_SDIO_SLOT_{config[CONF_SLOT]}", + True, + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_{config[CONF_SLOT]}", + config[CONF_CLK_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_{config[CONF_SLOT]}", + config[CONF_CMD_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_{config[CONF_SLOT]}", + config[CONF_D0_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_{config[CONF_SLOT]}", + config[CONF_D1_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_{config[CONF_SLOT]}", + config[CONF_D2_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_{config[CONF_SLOT]}", + config[CONF_D3_PIN], + ) + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_CUSTOM_SDIO_PINS", True) + + framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}" + esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.10.2") + esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") + esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11") + esp32.add_extra_script( + "post", + "esp32_hosted.py", + os.path.join(os.path.dirname(__file__), "esp32_hosted.py.script"), + ) diff --git a/esphome/components/esp32_hosted/esp32_hosted.py.script b/esphome/components/esp32_hosted/esp32_hosted.py.script new file mode 100644 index 0000000000..4be297c500 --- /dev/null +++ b/esphome/components/esp32_hosted/esp32_hosted.py.script @@ -0,0 +1,12 @@ +# pylint: disable=E0602 +Import("env") # noqa + +# Workaround whole archive issue +if "__LIB_DEPS" in env and "libespressif__esp_hosted.a" in env["__LIB_DEPS"]: + env.Append( + LINKFLAGS=[ + "-Wl,--whole-archive", + env["BUILD_DIR"] + "/esp-idf/espressif__esp_hosted/libespressif__esp_hosted.a", + "-Wl,--no-whole-archive", + ] + ) diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 8460de5638..7dcfe918eb 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -11,3 +11,15 @@ dependencies: path: components/mdns rules: - if: "idf_version >=5.0" + espressif/esp_wifi_remote: + version: 0.10.2 + rules: + - if: "target in [esp32h2, esp32p4]" + espressif/eppp_link: + version: 0.2.0 + rules: + - if: "target in [esp32h2, esp32p4]" + espressif/esp_hosted: + version: 2.0.11 + rules: + - if: "target in [esp32h2, esp32p4]" diff --git a/tests/components/esp32_hosted/common.yaml b/tests/components/esp32_hosted/common.yaml new file mode 100644 index 0000000000..ab029e5064 --- /dev/null +++ b/tests/components/esp32_hosted/common.yaml @@ -0,0 +1,15 @@ +esp32_hosted: + variant: ESP32C6 + slot: 1 + active_high: true + reset_pin: GPIO15 + cmd_pin: GPIO13 + clk_pin: GPIO12 + d0_pin: GPIO11 + d1_pin: GPIO10 + d2_pin: GPIO9 + d3_pin: GPIO8 + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/esp32_hosted/test.esp32-p4-idf.yaml b/tests/components/esp32_hosted/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_hosted/test.esp32-p4-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 61dfd5541f491d3306fc38106e5a8ba08513185f Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Fri, 27 Jun 2025 04:40:42 +0200 Subject: [PATCH 137/293] use c++17 `[[fallthrough]];` (#9149) --- esphome/components/audio/audio_decoder.cpp | 2 +- .../esp32_ble_client/ble_client_base.cpp | 10 +++++----- esphome/components/pn7150/pn7150.cpp | 14 +++++++------- esphome/components/pn7160/pn7160.cpp | 14 +++++++------- esphome/components/shelly_dimmer/stm32flash.cpp | 3 +-- 5 files changed, 21 insertions(+), 22 deletions(-) diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index c74b028c4b..90ba1aec1e 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -312,7 +312,7 @@ FileDecoderState AudioDecoder::decode_mp3_() { if (err) { switch (err) { case esp_audio_libs::helix_decoder::ERR_MP3_OUT_OF_MEMORY: - // Intentional fallthrough + [[fallthrough]]; case esp_audio_libs::helix_decoder::ERR_MP3_NULL_POINTER: return FileDecoderState::FAILED; break; diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 8ae1eb1bac..7d0a3bbfd5 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -496,17 +496,17 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { if (length > 2) { return (float) encode_uint16(value[1], value[2]); } - // fall through + [[fallthrough]]; case 0x7: // uint24. if (length > 3) { return (float) encode_uint24(value[1], value[2], value[3]); } - // fall through + [[fallthrough]]; case 0x8: // uint32. if (length > 4) { return (float) encode_uint32(value[1], value[2], value[3], value[4]); } - // fall through + [[fallthrough]]; case 0xC: // int8. return (float) ((int8_t) value[1]); case 0xD: // int12. @@ -514,12 +514,12 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { if (length > 2) { return (float) ((int16_t) (value[1] << 8) + (int16_t) value[2]); } - // fall through + [[fallthrough]]; case 0xF: // int24. if (length > 3) { return (float) ((int32_t) (value[1] << 16) + (int32_t) (value[2] << 8) + (int32_t) (value[3])); } - // fall through + [[fallthrough]]; case 0x10: // int32. if (length > 4) { return (float) ((int32_t) (value[1] << 24) + (int32_t) (value[2] << 16) + (int32_t) (value[3] << 8) + diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp index 971ddd23cb..f827bd151a 100644 --- a/esphome/components/pn7150/pn7150.cpp +++ b/esphome/components/pn7150/pn7150.cpp @@ -584,7 +584,7 @@ void PN7150::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::NFCC_INIT); } - // fall through + [[fallthrough]]; case NCIState::NFCC_INIT: if (this->init_core_() != nfc::STATUS_OK) { @@ -594,7 +594,7 @@ void PN7150::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); } - // fall through + [[fallthrough]]; case NCIState::NFCC_CONFIG: if (this->send_init_config_() != nfc::STATUS_OK) { @@ -605,7 +605,7 @@ void PN7150::nci_fsm_transition_() { this->config_refresh_pending_ = false; this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); } - // fall through + [[fallthrough]]; case NCIState::NFCC_SET_DISCOVER_MAP: if (this->set_discover_map_() != nfc::STATUS_OK) { @@ -615,7 +615,7 @@ void PN7150::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); } - // fall through + [[fallthrough]]; case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { @@ -625,7 +625,7 @@ void PN7150::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::RFST_IDLE); } - // fall through + [[fallthrough]]; case NCIState::RFST_IDLE: if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { @@ -650,14 +650,14 @@ void PN7150::nci_fsm_transition_() { case NCIState::RFST_W4_HOST_SELECT: select_endpoint_(); - // fall through + [[fallthrough]]; // All cases below are waiting for NOTIFICATION messages case NCIState::RFST_DISCOVERY: if (this->config_refresh_pending_) { this->refresh_core_config_(); } - // fall through + [[fallthrough]]; case NCIState::RFST_LISTEN_ACTIVE: case NCIState::RFST_LISTEN_SLEEP: diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp index 2a1de20657..a8edfadd8e 100644 --- a/esphome/components/pn7160/pn7160.cpp +++ b/esphome/components/pn7160/pn7160.cpp @@ -609,7 +609,7 @@ void PN7160::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::NFCC_INIT); } - // fall through + [[fallthrough]]; case NCIState::NFCC_INIT: if (this->init_core_() != nfc::STATUS_OK) { @@ -619,7 +619,7 @@ void PN7160::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); } - // fall through + [[fallthrough]]; case NCIState::NFCC_CONFIG: if (this->send_init_config_() != nfc::STATUS_OK) { @@ -630,7 +630,7 @@ void PN7160::nci_fsm_transition_() { this->config_refresh_pending_ = false; this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); } - // fall through + [[fallthrough]]; case NCIState::NFCC_SET_DISCOVER_MAP: if (this->set_discover_map_() != nfc::STATUS_OK) { @@ -640,7 +640,7 @@ void PN7160::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); } - // fall through + [[fallthrough]]; case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { @@ -650,7 +650,7 @@ void PN7160::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::RFST_IDLE); } - // fall through + [[fallthrough]]; case NCIState::RFST_IDLE: if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { @@ -675,14 +675,14 @@ void PN7160::nci_fsm_transition_() { case NCIState::RFST_W4_HOST_SELECT: select_endpoint_(); - // fall through + [[fallthrough]]; // All cases below are waiting for NOTIFICATION messages case NCIState::RFST_DISCOVERY: if (this->config_refresh_pending_) { this->refresh_core_config_(); } - // fall through + [[fallthrough]]; case NCIState::RFST_LISTEN_ACTIVE: case NCIState::RFST_LISTEN_SLEEP: diff --git a/esphome/components/shelly_dimmer/stm32flash.cpp b/esphome/components/shelly_dimmer/stm32flash.cpp index 3871d89a2f..b052c0cee9 100644 --- a/esphome/components/shelly_dimmer/stm32flash.cpp +++ b/esphome/components/shelly_dimmer/stm32flash.cpp @@ -445,8 +445,7 @@ template stm32_err_t stm32_check_ack_timeout(const stm32_err_t s_err return STM32_ERR_OK; case STM32_ERR_NACK: log(); - // TODO: c++17 [[fallthrough]] - /* fallthrough */ + [[fallthrough]]; default: return STM32_ERR_UNKNOWN; } From 1f94e4cc14f5225270875f62d83d3b7b45d6f80e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 26 Jun 2025 23:37:30 -0400 Subject: [PATCH 138/293] [esp32] Update IDF components to use the registry (#9223) --- esphome/components/esp32_camera/__init__.py | 6 +----- esphome/components/mdns/__init__.py | 7 +------ esphome/components/micro_wake_word/__init__.py | 6 +----- esphome/idf_component.yml | 18 ++++++------------ 4 files changed, 9 insertions(+), 28 deletions(-) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index cfca0ed6fc..8dc2ede372 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -310,11 +310,7 @@ async def to_code(config): cg.add_define("USE_ESP32_CAMERA") if CORE.using_esp_idf: - add_idf_component( - name="esp32-camera", - repo="https://github.com/espressif/esp32-camera.git", - ref="v2.0.15", - ) + add_idf_component(name="espressif/esp32-camera", ref="2.0.15") for conf in config.get(CONF_ON_STREAM_START, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 4b5e40dfea..2f81068e8a 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -88,12 +88,7 @@ async def to_code(config): if CORE.using_esp_idf and CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version( 5, 0, 0 ): - add_idf_component( - name="mdns", - repo="https://github.com/espressif/esp-protocols.git", - ref="mdns-v1.8.2", - path="components/mdns", - ) + add_idf_component(name="espressif/mdns", ref="1.8.2") cg.add_define("USE_MDNS") diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 0efe2ac288..cde8752157 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -449,11 +449,7 @@ async def to_code(config): cg.add_define("USE_MICRO_WAKE_WORD") cg.add_define("USE_OTA_STATE_CALLBACK") - esp32.add_idf_component( - name="esp-tflite-micro", - repo="https://github.com/espressif/esp-tflite-micro", - ref="v1.3.3.1", - ) + esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1") cg.add_build_flag("-DTF_LITE_STATIC_MEMORY") cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 7dcfe918eb..6299909033 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -1,16 +1,10 @@ dependencies: - esp-tflite-micro: - git: https://github.com/espressif/esp-tflite-micro.git - version: v1.3.1 - esp32_camera: - git: https://github.com/espressif/esp32-camera.git - version: v2.0.15 - mdns: - git: https://github.com/espressif/esp-protocols.git - version: mdns-v1.8.2 - path: components/mdns - rules: - - if: "idf_version >=5.0" + espressif/esp-tflite-micro: + version: 1.3.3~1 + espressif/esp32-camera: + version: 2.0.15 + espressif/mdns: + version: 1.8.2 espressif/esp_wifi_remote: version: 0.10.2 rules: From 62f28902c5d54793efeded201eb4059074bdf9e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 08:50:26 +0200 Subject: [PATCH 139/293] [wifi] Reduce memory usage (#9232) --- esphome/components/wifi/wifi_component.h | 48 +++++++++++++----------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index efd43077d1..64797a5801 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -62,7 +62,7 @@ struct SavedWifiFastConnectSettings { uint8_t channel; } PACKED; // NOLINT -enum WiFiComponentState { +enum WiFiComponentState : uint8_t { /** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */ WIFI_COMPONENT_STATE_OFF = 0, /** WiFi is disabled. */ @@ -146,14 +146,14 @@ class WiFiAP { protected: std::string ssid_; - optional bssid_; std::string password_; + optional bssid_; #ifdef USE_WIFI_WPA2_EAP optional eap_; #endif // USE_WIFI_WPA2_EAP - optional channel_; - float priority_{0}; optional manual_ip_; + float priority_{0}; + optional channel_; bool hidden_{false}; }; @@ -177,14 +177,14 @@ class WiFiScanResult { bool operator==(const WiFiScanResult &rhs) const; protected: - bool matches_{false}; bssid_t bssid_; std::string ssid_; + float priority_{0.0f}; uint8_t channel_; int8_t rssi_; + bool matches_{false}; bool with_auth_; bool is_hidden_; - float priority_{0.0f}; }; struct WiFiSTAPriority { @@ -192,7 +192,7 @@ struct WiFiSTAPriority { float priority; }; -enum WiFiPowerSaveMode { +enum WiFiPowerSaveMode : uint8_t { WIFI_POWER_SAVE_NONE = 0, WIFI_POWER_SAVE_LIGHT, WIFI_POWER_SAVE_HIGH, @@ -383,28 +383,36 @@ class WiFiComponent : public Component { std::string use_address_; std::vector sta_; std::vector sta_priorities_; + std::vector scan_result_; WiFiAP selected_ap_; - bool fast_connect_{false}; - bool retry_hidden_{false}; - - bool has_ap_{false}; WiFiAP ap_; - WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; - bool handled_connected_state_{false}; + optional output_power_; + ESPPreferenceObject pref_; + ESPPreferenceObject fast_connect_pref_; + + // Group all 32-bit integers together uint32_t action_started_; - uint8_t num_retried_{0}; uint32_t last_connected_{0}; uint32_t reboot_timeout_{}; uint32_t ap_timeout_{}; + + // Group all 8-bit values together + WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; + uint8_t num_retried_{0}; +#if USE_NETWORK_IPV6 + uint8_t num_ipv6_addresses_{0}; +#endif /* USE_NETWORK_IPV6 */ + + // Group all boolean values together + bool fast_connect_{false}; + bool retry_hidden_{false}; + bool has_ap_{false}; + bool handled_connected_state_{false}; bool error_from_callback_{false}; - std::vector scan_result_; bool scan_done_{false}; bool ap_setup_{false}; - optional output_power_; bool passive_scan_{false}; - ESPPreferenceObject pref_; - ESPPreferenceObject fast_connect_pref_; bool has_saved_wifi_settings_{false}; #ifdef USE_WIFI_11KV_SUPPORT bool btm_{false}; @@ -412,10 +420,8 @@ class WiFiComponent : public Component { #endif bool enable_on_boot_; bool got_ipv4_address_{false}; -#if USE_NETWORK_IPV6 - uint8_t num_ipv6_addresses_{0}; -#endif /* USE_NETWORK_IPV6 */ + // Pointers at the end (naturally aligned) Trigger<> *connect_trigger_{new Trigger<>()}; Trigger<> *disconnect_trigger_{new Trigger<>()}; }; From 7931423e8c99cd0a22d16f2f84d75c0d4f79aed6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 08:52:12 +0200 Subject: [PATCH 140/293] Reduce ethernet component memory usage by 8 bytes (#9231) --- .../components/ethernet/ethernet_component.h | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 7a205d89f0..0f0eff5ded 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -15,7 +15,7 @@ namespace esphome { namespace ethernet { -enum EthernetType { +enum EthernetType : uint8_t { ETHERNET_TYPE_UNKNOWN = 0, ETHERNET_TYPE_LAN8720, ETHERNET_TYPE_RTL8201, @@ -42,7 +42,7 @@ struct PHYRegister { uint32_t page; }; -enum class EthernetComponentState { +enum class EthernetComponentState : uint8_t { STOPPED, CONNECTING, CONNECTED, @@ -119,25 +119,31 @@ class EthernetComponent : public Component { uint32_t polling_interval_{0}; #endif #else - uint8_t phy_addr_{0}; + // Group all 32-bit members first int power_pin_{-1}; - uint8_t mdc_pin_{23}; - uint8_t mdio_pin_{18}; emac_rmii_clock_mode_t clk_mode_{EMAC_CLK_EXT_IN}; emac_rmii_clock_gpio_t clk_gpio_{EMAC_CLK_IN_GPIO}; std::vector phy_registers_{}; -#endif - EthernetType type_{ETHERNET_TYPE_UNKNOWN}; - optional manual_ip_{}; + // Group all 8-bit members together + uint8_t phy_addr_{0}; + uint8_t mdc_pin_{23}; + uint8_t mdio_pin_{18}; +#endif + optional manual_ip_{}; + uint32_t connect_begin_; + + // Group all uint8_t types together (enums and bools) + EthernetType type_{ETHERNET_TYPE_UNKNOWN}; + EthernetComponentState state_{EthernetComponentState::STOPPED}; bool started_{false}; bool connected_{false}; bool got_ipv4_address_{false}; #if LWIP_IPV6 uint8_t ipv6_count_{0}; #endif /* LWIP_IPV6 */ - EthernetComponentState state_{EthernetComponentState::STOPPED}; - uint32_t connect_begin_; + + // Pointers at the end (naturally aligned) esp_netif_t *eth_netif_{nullptr}; esp_eth_handle_t eth_handle_; esp_eth_phy_t *phy_{nullptr}; From 13512440ac57954c0246933a9f80e9455a1ab1d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 08:53:40 +0200 Subject: [PATCH 141/293] [gpio] Reduce ESP32 memory usage by optimizing struct padding (#9230) --- esphome/components/esp32/gpio.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/gpio.h b/esphome/components/esp32/gpio.h index d69ac1c493..0fefc1c058 100644 --- a/esphome/components/esp32/gpio.h +++ b/esphome/components/esp32/gpio.h @@ -29,9 +29,9 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; gpio_num_t pin_; - bool inverted_; gpio_drive_cap_t drive_strength_; gpio::Flags flags_; + bool inverted_; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static bool isr_service_installed; }; From 837dd46adffe2f6dbb4184f54b5a34b79acb8bec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 08:56:54 +0200 Subject: [PATCH 142/293] Reduce component_iterator memory usage (#9205) --- esphome/core/component_iterator.cpp | 2 +- esphome/core/component_iterator.h | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index da593340c1..b06c964b7c 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -375,7 +375,7 @@ void ComponentIterator::advance() { } if (advance_platform) { - this->state_ = static_cast(static_cast(this->state_) + 1); + this->state_ = static_cast(static_cast(this->state_) + 1); this->at_ = 0; } else if (success) { this->at_++; diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index 9e187f6c57..4b41872db7 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -93,7 +93,9 @@ class ComponentIterator { virtual bool on_end(); protected: - enum class IteratorState { + // Iterates over all ESPHome entities (sensors, switches, lights, etc.) + // Supports up to 256 entity types and up to 65,535 entities of each type + enum class IteratorState : uint8_t { NONE = 0, BEGIN, #ifdef USE_BINARY_SENSOR @@ -167,7 +169,7 @@ class ComponentIterator { #endif MAX, } state_{IteratorState::NONE}; - size_t at_{0}; + uint16_t at_{0}; // Supports up to 65,535 entities per type bool include_internal_{false}; }; From c0b1f32889c6e240aa8b52b0e19d708a2c2342a1 Mon Sep 17 00:00:00 2001 From: scaiper Date: Fri, 27 Jun 2025 13:43:18 +0300 Subject: [PATCH 143/293] [esp32] Change ``enable_lwip_mdns_queries`` default to ``True`` (#9188) --- esphome/components/esp32/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 4e2a6ab852..8319ed5e74 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -610,7 +610,7 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False ): cv.boolean, cv.Optional( - CONF_ENABLE_LWIP_MDNS_QUERIES, default=False + CONF_ENABLE_LWIP_MDNS_QUERIES, default=True ): cv.boolean, cv.Optional( CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False @@ -770,7 +770,7 @@ async def to_code(config): and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER] ): add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) - if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, False): + if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True): add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) From 68d66c873e62be045c55d7dcada54bee3cc3636d Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Fri, 27 Jun 2025 19:31:50 +0200 Subject: [PATCH 144/293] Upgrade to use C++20 (#9135) Co-authored-by: J. Nick Koston --- esphome/components/ac_dimmer/ac_dimmer.cpp | 3 ++- esphome/components/atm90e32/atm90e32.cpp | 3 ++- esphome/components/display/display.cpp | 7 ++++--- esphome/components/display/display.h | 2 -- esphome/components/esp32/__init__.py | 2 +- esphome/components/esp8266/__init__.py | 2 +- esphome/components/esp8266/gpio.cpp | 20 +++++++++---------- esphome/components/host/__init__.py | 2 +- .../http_request/ota/ota_http_request.cpp | 2 +- .../components/hydreon_rgxx/hydreon_rgxx.cpp | 12 +++-------- esphome/components/libretiny/__init__.py | 2 +- .../components/online_image/online_image.cpp | 2 +- .../pulse_meter/pulse_meter_sensor.cpp | 8 ++++---- .../radon_eye_ble/radon_eye_listener.cpp | 2 +- .../remote_receiver_esp8266.cpp | 2 +- esphome/components/rp2040/__init__.py | 2 +- esphome/components/sn74hc595/sn74hc595.cpp | 9 +++++---- esphome/components/sun/sun.cpp | 7 +++---- esphome/components/tx20/tx20.cpp | 4 ++-- esphome/components/wiegand/wiegand.cpp | 4 ++-- esphome/core/application.cpp | 17 ++++++++-------- esphome/cpp_generator.py | 2 +- platformio.ini | 6 +++--- 23 files changed, 59 insertions(+), 63 deletions(-) diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index 276adeebb0..e6f7a1214a 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -4,6 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include +#include #ifdef USE_ESP8266 #include @@ -203,7 +204,7 @@ void AcDimmer::setup() { #endif } void AcDimmer::write_state(float state) { - state = std::acos(1 - (2 * state)) / 3.14159; // RMS power compensation + state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation auto new_value = static_cast(roundf(state * 65535)); if (new_value != 0 && this->store_.value == 0) this->store_.init_cycle = this->init_with_half_cycle_; diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index f05d462845..4669a59e39 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -1,6 +1,7 @@ #include "atm90e32.h" #include #include +#include #include "esphome/core/log.h" namespace esphome { @@ -848,7 +849,7 @@ uint16_t ATM90E32Component::calculate_voltage_threshold(int line_freq, uint16_t float nominal_voltage = (line_freq == 60) ? 120.0f : 220.0f; float target_voltage = nominal_voltage * multiplier; - float peak_01v = target_voltage * 100.0f * std::sqrt(2.0f); // convert RMS → peak, scale to 0.01V + float peak_01v = target_voltage * 100.0f * std::numbers::sqrt2_v; // convert RMS → peak, scale to 0.01V float divider = (2.0f * ugain) / 32768.0f; float threshold = peak_01v / divider; diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index a13464ce1b..c666eee298 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -1,5 +1,6 @@ #include "display.h" #include +#include #include "display_color_utils.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" @@ -424,15 +425,15 @@ void HOT Display::get_regular_polygon_vertex(int vertex_id, int *vertex_x, int * // hence we rotate the shape by 270° to orient the polygon up. rotation_degrees += ROTATION_270_DEGREES; // Convert the rotation to radians, easier to use in trigonometrical calculations - float rotation_radians = rotation_degrees * PI / 180; + float rotation_radians = rotation_degrees * std::numbers::pi / 180; // A pointy top variation means the first vertex of the polygon is at the top center of the shape, this requires no // additional rotation of the shape. // A flat top variation means the first point of the polygon has to be rotated so that the first edge is horizontal, // this requires to rotate the shape by Ï€/edges radians counter-clockwise so that the first point is located on the // left side of the first horizontal edge. - rotation_radians -= (variation == VARIATION_FLAT_TOP) ? PI / edges : 0.0; + rotation_radians -= (variation == VARIATION_FLAT_TOP) ? std::numbers::pi / edges : 0.0; - float vertex_angle = ((float) vertex_id) / edges * 2 * PI + rotation_radians; + float vertex_angle = ((float) vertex_id) / edges * 2 * std::numbers::pi + rotation_radians; *vertex_x = (int) round(cos(vertex_angle) * radius) + center_x; *vertex_y = (int) round(sin(vertex_angle) * radius) + center_y; } diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 68c1184721..f2d79d12d9 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -138,8 +138,6 @@ enum DisplayRotation { DISPLAY_ROTATION_270_DEGREES = 270, }; -#define PI 3.1415926535897932384626433832795 - const int EDGES_TRIGON = 3; const int EDGES_TRIANGLE = 3; const int EDGES_TETRAGON = 4; diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 8319ed5e74..7e1c71a7de 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -704,7 +704,7 @@ FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate) async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) - cg.set_cpp_standard("gnu++17") + cg.set_cpp_standard("gnu++20") cg.add_build_flag("-DUSE_ESP32") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 4b4862a1d0..81daad8c56 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -183,7 +183,7 @@ async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_build_flag("-DUSE_ESP8266") - cg.set_cpp_standard("gnu++17") + cg.set_cpp_standard("gnu++20") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "ESP8266") diff --git a/esphome/components/esp8266/gpio.cpp b/esphome/components/esp8266/gpio.cpp index 4a997a790c..ee3683c67d 100644 --- a/esphome/components/esp8266/gpio.cpp +++ b/esphome/components/esp8266/gpio.cpp @@ -129,9 +129,9 @@ void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { } } else { if (value != arg->inverted) { - *arg->out_set_reg |= 1; + *arg->out_set_reg = *arg->out_set_reg | 1; } else { - *arg->out_set_reg &= ~1; + *arg->out_set_reg = *arg->out_set_reg & ~1; } } } @@ -147,7 +147,7 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { if (flags & gpio::FLAG_OUTPUT) { *arg->mode_set_reg = arg->mask; if (flags & gpio::FLAG_OPEN_DRAIN) { - *arg->control_reg |= 1 << GPCD; + *arg->control_reg = *arg->control_reg | (1 << GPCD); } else { *arg->control_reg &= ~(1 << GPCD); } @@ -155,21 +155,21 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { *arg->mode_clr_reg = arg->mask; } if (flags & gpio::FLAG_PULLUP) { - *arg->func_reg |= 1 << GPFPU; - *arg->control_reg |= 1 << GPCD; + *arg->func_reg = *arg->func_reg | (1 << GPFPU); + *arg->control_reg = *arg->control_reg | (1 << GPCD); } else { - *arg->func_reg &= ~(1 << GPFPU); + *arg->func_reg = *arg->func_reg & ~(1 << GPFPU); } } else { if (flags & gpio::FLAG_OUTPUT) { - *arg->mode_set_reg |= 1; + *arg->mode_set_reg = *arg->mode_set_reg | 1; } else { - *arg->mode_set_reg &= ~1; + *arg->mode_set_reg = *arg->mode_set_reg & ~1; } if (flags & gpio::FLAG_PULLDOWN) { - *arg->func_reg |= 1 << GP16FPD; + *arg->func_reg = *arg->func_reg | (1 << GP16FPD); } else { - *arg->func_reg &= ~(1 << GP16FPD); + *arg->func_reg = *arg->func_reg & ~(1 << GP16FPD); } } } diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index b59d8ebd03..da75873eaf 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -41,6 +41,6 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): cg.add_build_flag("-DUSE_HOST") cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) - cg.add_build_flag("-std=gnu++17") + cg.add_build_flag("-std=gnu++20") cg.add_define("ESPHOME_BOARD", "host") cg.add_platformio_option("platform", "platformio/native") diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 4c8d49dad5..4d9e868c74 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -258,7 +258,7 @@ bool OtaHttpRequestComponent::http_get_md5_() { } bool OtaHttpRequestComponent::validate_url_(const std::string &url) { - if ((url.length() < 8) || (url.find("http") != 0) || (url.find("://") == std::string::npos)) { + if ((url.length() < 8) || !url.starts_with("http") || (url.find("://") == std::string::npos)) { ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); return false; } diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp index 2d8381b60c..9d4680fdf4 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -159,12 +159,6 @@ void HydreonRGxxComponent::schedule_reboot_() { }); } -bool HydreonRGxxComponent::buffer_starts_with_(const std::string &prefix) { - return this->buffer_starts_with_(prefix.c_str()); -} - -bool HydreonRGxxComponent::buffer_starts_with_(const char *prefix) { return buffer_.rfind(prefix, 0) == 0; } - void HydreonRGxxComponent::process_line_() { ESP_LOGV(TAG, "Read from serial: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); @@ -191,7 +185,7 @@ void HydreonRGxxComponent::process_line_() { ESP_LOGW(TAG, "Received EmSat!"); this->em_sat_ = true; } - if (this->buffer_starts_with_("PwrDays")) { + if (buffer_.starts_with("PwrDays")) { if (this->boot_count_ <= 0) { this->boot_count_ = 1; } else { @@ -220,7 +214,7 @@ void HydreonRGxxComponent::process_line_() { } return; } - if (this->buffer_starts_with_("SW")) { + if (buffer_.starts_with("SW")) { std::string::size_type majend = this->buffer_.find('.'); std::string::size_type endversion = this->buffer_.find(' ', 3); if (majend == std::string::npos || endversion == std::string::npos || majend > endversion) { @@ -282,7 +276,7 @@ void HydreonRGxxComponent::process_line_() { } } else { for (const auto *ignore : IGNORE_STRINGS) { - if (this->buffer_starts_with_(ignore)) { + if (buffer_.starts_with(ignore)) { ESP_LOGI(TAG, "Ignoring %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); return; } diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 28ee1e702f..149e5d1179 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -264,7 +264,7 @@ async def component_to_code(config): # force using arduino framework cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") - cg.set_cpp_standard("gnu++17") + cg.set_cpp_standard("gnu++20") # disable library compatibility checks cg.add_platformio_option("lib_ldf_mode", "off") diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index e21b2528d5..d0c743ef93 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -344,7 +344,7 @@ void OnlineImage::end_connection_() { } bool OnlineImage::validate_url_(const std::string &url) { - if ((url.length() < 8) || (url.find("http") != 0) || (url.find("://") == std::string::npos)) { + if ((url.length() < 8) || !url.starts_with("http") || (url.find("://") == std::string::npos)) { ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); return false; } diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index b82cb7a15c..81ecf22c71 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -63,7 +63,7 @@ void PulseMeterSensor::loop() { // If an edge was peeked, repay the debt if (this->peeked_edge_ && this->get_->count_ > 0) { this->peeked_edge_ = false; - this->get_->count_--; + this->get_->count_--; // NOLINT(clang-diagnostic-deprecated-volatile) } // If there is an unprocessed edge, and filter_us_ has passed since, count this edge early @@ -71,7 +71,7 @@ void PulseMeterSensor::loop() { now - this->get_->last_rising_edge_us_ >= this->filter_us_) { this->peeked_edge_ = true; this->get_->last_detected_edge_us_ = this->get_->last_rising_edge_us_; - this->get_->count_++; + this->get_->count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // Check if we detected a pulse this loop @@ -146,7 +146,7 @@ void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) { state.last_sent_edge_us_ = now; set.last_detected_edge_us_ = now; set.last_rising_edge_us_ = now; - set.count_++; + set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // This ISR is bound to rising edges, so the pin is high @@ -169,7 +169,7 @@ void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) { } else if (length && !state.latched_ && sensor->last_pin_val_) { // Long enough high edge state.latched_ = true; set.last_detected_edge_us_ = state.last_intr_; - set.count_++; + set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // Due to order of operations this includes diff --git a/esphome/components/radon_eye_ble/radon_eye_listener.cpp b/esphome/components/radon_eye_ble/radon_eye_listener.cpp index a4c79db753..0c6165c691 100644 --- a/esphome/components/radon_eye_ble/radon_eye_listener.cpp +++ b/esphome/components/radon_eye_ble/radon_eye_listener.cpp @@ -17,7 +17,7 @@ bool RadonEyeListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device // Check if the device name starts with any of the prefixes if (std::any_of(prefixes.begin(), prefixes.end(), - [&](const std::string &prefix) { return device.get_name().rfind(prefix, 0) == 0; })) { + [&](const std::string &prefix) { return device.get_name().starts_with(prefix); })) { // Device found ESP_LOGD(TAG, "Found Radon Eye device Name: %s (MAC: %s)", device.get_name().c_str(), device.address_str().c_str()); diff --git a/esphome/components/remote_receiver/remote_receiver_esp8266.cpp b/esphome/components/remote_receiver/remote_receiver_esp8266.cpp index a0fd56bcf4..fe935ba227 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp8266.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp8266.cpp @@ -27,7 +27,7 @@ void IRAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverCompone if (time_since_change <= arg->filter_us) return; - arg->buffer[arg->buffer_write_at = next] = now; + arg->buffer[arg->buffer_write_at = next] = now; // NOLINT(clang-diagnostic-deprecated-volatile) } void RemoteReceiverComponent::setup() { diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 2718e3050f..ecbeb83bb4 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -167,7 +167,7 @@ async def to_code(config): cg.add_platformio_option("lib_ldf_mode", "chain+") cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_build_flag("-DUSE_RP2040") - cg.set_cpp_standard("gnu++17") + cg.set_cpp_standard("gnu++20") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "RP2040") diff --git a/esphome/components/sn74hc595/sn74hc595.cpp b/esphome/components/sn74hc595/sn74hc595.cpp index f8d24b898f..d8e33eec22 100644 --- a/esphome/components/sn74hc595/sn74hc595.cpp +++ b/esphome/components/sn74hc595/sn74hc595.cpp @@ -1,5 +1,6 @@ #include "sn74hc595.h" #include "esphome/core/log.h" +#include namespace esphome { namespace sn74hc595 { @@ -55,9 +56,9 @@ void SN74HC595Component::digital_write_(uint16_t pin, bool value) { } void SN74HC595GPIOComponent::write_gpio() { - for (auto byte = this->output_bytes_.rbegin(); byte != this->output_bytes_.rend(); byte++) { + for (uint8_t &output_byte : std::ranges::reverse_view(this->output_bytes_)) { for (int8_t i = 7; i >= 0; i--) { - bool bit = (*byte >> i) & 1; + bool bit = (output_byte >> i) & 1; this->data_pin_->digital_write(bit); this->clock_pin_->digital_write(true); this->clock_pin_->digital_write(false); @@ -68,9 +69,9 @@ void SN74HC595GPIOComponent::write_gpio() { #ifdef USE_SPI void SN74HC595SPIComponent::write_gpio() { - for (auto byte = this->output_bytes_.rbegin(); byte != this->output_bytes_.rend(); byte++) { + for (uint8_t &output_byte : std::ranges::reverse_view(this->output_bytes_)) { this->enable(); - this->transfer_byte(*byte); + this->transfer_byte(output_byte); this->disable(); } SN74HC595Component::write_gpio(); diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index 2fd9394a5e..df7030461b 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -1,5 +1,6 @@ #include "sun.h" #include "esphome/core/log.h" +#include /* The formulas/algorithms in this module are based on the book @@ -18,14 +19,12 @@ using namespace esphome::sun::internal; static const char *const TAG = "sun"; -#undef PI #undef degrees #undef radians #undef sq -static const num_t PI = 3.141592653589793; -inline num_t degrees(num_t rad) { return rad * 180 / PI; } -inline num_t radians(num_t deg) { return deg * PI / 180; } +inline num_t degrees(num_t rad) { return rad * 180 / std::numbers::pi; } +inline num_t radians(num_t deg) { return deg * std::numbers::pi / 180; } inline num_t arcdeg(num_t deg, num_t minutes, num_t seconds) { return deg + minutes / 60 + seconds / 3600; } inline num_t sq(num_t x) { return x * x; } inline num_t cb(num_t x) { return x * x * x; } diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp index 73b865e8b8..42e3955fc2 100644 --- a/esphome/components/tx20/tx20.cpp +++ b/esphome/components/tx20/tx20.cpp @@ -152,7 +152,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { } arg->buffer[arg->buffer_index] = 1; arg->start_time = now; - arg->buffer_index++; + arg->buffer_index++; // NOLINT(clang-diagnostic-deprecated-volatile) return; } const uint32_t delay = now - arg->start_time; @@ -183,7 +183,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { } arg->spent_time += delay; arg->start_time = now; - arg->buffer_index++; + arg->buffer_index++; // NOLINT(clang-diagnostic-deprecated-volatile) } void IRAM_ATTR Tx20ComponentStore::reset() { tx20_available = false; diff --git a/esphome/components/wiegand/wiegand.cpp b/esphome/components/wiegand/wiegand.cpp index 5a2bb8deee..dd1443d10c 100644 --- a/esphome/components/wiegand/wiegand.cpp +++ b/esphome/components/wiegand/wiegand.cpp @@ -11,7 +11,7 @@ static const char *const KEYS = "0123456789*#"; void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) { if (arg->d0.digital_read()) return; - arg->count++; + arg->count++; // NOLINT(clang-diagnostic-deprecated-volatile) arg->value <<= 1; arg->last_bit_time = millis(); arg->done = false; @@ -20,7 +20,7 @@ void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) { void IRAM_ATTR HOT WiegandStore::d1_gpio_intr(WiegandStore *arg) { if (arg->d1.digital_read()) return; - arg->count++; + arg->count++; // NOLINT(clang-diagnostic-deprecated-volatile) arg->value = (arg->value << 1) | 1; arg->last_bit_time = millis(); arg->done = false; diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index f64070fa3d..328de00640 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -3,6 +3,7 @@ #include "esphome/core/version.h" #include "esphome/core/hal.h" #include +#include #ifdef USE_STATUS_LED #include "esphome/components/status_led/status_led.h" @@ -184,8 +185,8 @@ void IRAM_ATTR HOT Application::feed_wdt(uint32_t time) { } void Application::reboot() { ESP_LOGI(TAG, "Forcing a reboot"); - for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { - (*it)->on_shutdown(); + for (auto &component : std::ranges::reverse_view(this->components_)) { + component->on_shutdown(); } arch_restart(); } @@ -198,17 +199,17 @@ void Application::safe_reboot() { } void Application::run_safe_shutdown_hooks() { - for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { - (*it)->on_safe_shutdown(); + for (auto &component : std::ranges::reverse_view(this->components_)) { + component->on_safe_shutdown(); } - for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { - (*it)->on_shutdown(); + for (auto &component : std::ranges::reverse_view(this->components_)) { + component->on_shutdown(); } } void Application::run_powerdown_hooks() { - for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { - (*it)->on_powerdown(); + for (auto &component : std::ranges::reverse_view(this->components_)) { + component->on_powerdown(); } } diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 2a7b7fe057..060dd36f8f 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -617,7 +617,7 @@ def set_cpp_standard(standard: str) -> None: """Set C++ standard with compiler flag `-std={standard}`.""" CORE.add_build_unflag("-std=gnu++11") CORE.add_build_unflag("-std=gnu++14") - CORE.add_build_unflag("-std=gnu++20") + CORE.add_build_unflag("-std=gnu++17") CORE.add_build_unflag("-std=gnu++23") CORE.add_build_unflag("-std=gnu++2a") CORE.add_build_unflag("-std=gnu++2b") diff --git a/platformio.ini b/platformio.ini index 6da9fc1338..be9d7587c2 100644 --- a/platformio.ini +++ b/platformio.ini @@ -47,11 +47,11 @@ lib_deps = lvgl/lvgl@8.4.0 ; lvgl build_flags = -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE - -std=gnu++17 + -std=gnu++20 build_unflags = -std=gnu++11 -std=gnu++14 - -std=gnu++20 + -std=gnu++17 -std=gnu++23 -std=gnu++2a -std=gnu++2b @@ -560,7 +560,7 @@ lib_deps = build_flags = ${common.build_flags} -DUSE_HOST - -std=c++17 + -std=c++20 build_unflags = ${common.build_unflags} From 156a9160ba9ac191543ae7c6c844ba259d0cd59b Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Fri, 27 Jun 2025 18:31:23 -0700 Subject: [PATCH 145/293] [mcp23xxx_base] fix pin interrupts (#9244) Co-authored-by: Samuel Sieb --- esphome/components/mcp23xxx_base/mcp23xxx_base.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp index 14a703fb9f..fc49f216ee 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp @@ -6,7 +6,11 @@ namespace mcp23xxx_base { float MCP23XXXBase::get_setup_priority() const { return setup_priority::IO; } -void MCP23XXXGPIOPin::setup() { pin_mode(flags_); } +void MCP23XXXGPIOPin::setup() { + pin_mode(flags_); + this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_); +} + void MCP23XXXGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool MCP23XXXGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void MCP23XXXGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } From 52ca8deb1099eca9be1ef9a4fa5c30f2edf67343 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Jun 2025 21:32:18 -0400 Subject: [PATCH 146/293] [i2c] Disable i2c scan on certain idf versions (#9237) --- esphome/components/i2c/__init__.py | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index d56bb2d07c..fae1fa1d22 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -1,5 +1,8 @@ +import logging + from esphome import pins import esphome.codegen as cg +from esphome.components import esp32 import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, @@ -12,6 +15,8 @@ from esphome.const import ( CONF_SCL, CONF_SDA, CONF_TIMEOUT, + KEY_CORE, + KEY_FRAMEWORK_VERSION, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, @@ -19,6 +24,7 @@ from esphome.const import ( from esphome.core import CORE, coroutine_with_priority import esphome.final_validate as fv +LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] i2c_ns = cg.esphome_ns.namespace("i2c") I2CBus = i2c_ns.class_("I2CBus") @@ -41,6 +47,32 @@ def _bus_declare_type(value): raise NotImplementedError +def validate_config(config): + if ( + config[CONF_SCAN] + and CORE.is_esp32 + and CORE.using_esp_idf + and esp32.get_esp32_variant() + in [ + esp32.const.VARIANT_ESP32C5, + esp32.const.VARIANT_ESP32C6, + esp32.const.VARIANT_ESP32P4, + ] + ): + version: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + if version.major == 5 and ( + (version.minor == 3 and version.patch <= 3) + or (version.minor == 4 and version.patch <= 1) + ): + LOGGER.warning( + "There is a bug in esp-idf version %s that breaks I2C scan, I2C scan " + "has been disabled, see https://github.com/esphome/issues/issues/7128", + str(version), + ) + config[CONF_SCAN] = False + return config + + pin_with_input_and_output_support = pins.internal_gpio_pin_number( {CONF_OUTPUT: True, CONF_INPUT: True} ) @@ -66,6 +98,7 @@ CONFIG_SCHEMA = cv.All( } ).extend(cv.COMPONENT_SCHEMA), cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), + validate_config, ) From f8d59b5aeb1268d52a47fe674b115e7245ceb143 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 22:53:40 -0500 Subject: [PATCH 147/293] Reduce libretiny logconfig messages (#9239) --- esphome/components/libretiny/lt_component.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/libretiny/lt_component.cpp b/esphome/components/libretiny/lt_component.cpp index ec4b60eaeb..ffccd0ad7a 100644 --- a/esphome/components/libretiny/lt_component.cpp +++ b/esphome/components/libretiny/lt_component.cpp @@ -10,9 +10,11 @@ namespace libretiny { static const char *const TAG = "lt.component"; void LTComponent::dump_config() { - ESP_LOGCONFIG(TAG, "LibreTiny:"); - ESP_LOGCONFIG(TAG, " Version: %s", LT_BANNER_STR + 10); - ESP_LOGCONFIG(TAG, " Loglevel: %u", LT_LOGLEVEL); + ESP_LOGCONFIG(TAG, + "LibreTiny:\n" + " Version: %s\n" + " Loglevel: %u", + LT_BANNER_STR + 10, LT_LOGLEVEL); #ifdef USE_TEXT_SENSOR if (this->version_ != nullptr) { From 094bf19ec4d86de89d57cc60a3870bdcc6cc4567 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 23:58:53 -0500 Subject: [PATCH 148/293] Disable dynamic log level control for ESP32 ESP-IDF builds (#9233) --- esphome/components/esp32/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 7e1c71a7de..6425e57b13 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -758,6 +758,9 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) + # Disable dynamic log level control to save memory + add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False) + # Set default CPU frequency add_idf_sdkconfig_option(f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{freq}", True) From 3f65cee17cb135895921f0309d01af404d96d61a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 23:59:52 -0500 Subject: [PATCH 149/293] Silence protobuf compatibility warnings when importing aioesphomeapi (#9236) --- esphome/components/api/client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 20136ef7b8..7f8e2529a5 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -4,9 +4,15 @@ import asyncio from datetime import datetime import logging from typing import TYPE_CHECKING, Any +import warnings -from aioesphomeapi import APIClient, parse_log_message -from aioesphomeapi.log_runner import async_run +# Suppress protobuf version warnings +with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", category=UserWarning, message=".*Protobuf gencode version.*" + ) + from aioesphomeapi import APIClient, parse_log_message + from aioesphomeapi.log_runner import async_run from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ from esphome.core import CORE From d37f5b87bd024a14eb4828076b0a32f166d6f903 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 28 Jun 2025 02:30:59 -0400 Subject: [PATCH 150/293] [esp32] Allow 5.4.2 (#9243) --- esphome/components/esp32/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 6425e57b13..32323b7504 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -341,6 +341,7 @@ SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ # List based on https://github.com/pioarduino/esp-idf/releases SUPPORTED_PIOARDUINO_ESP_IDF_5X = [ cv.Version(5, 5, 0), + cv.Version(5, 4, 2), cv.Version(5, 4, 1), cv.Version(5, 4, 0), cv.Version(5, 3, 3), From 58b7d0b412187aa4fb0c8ec03444b5e23454d39b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Jun 2025 10:21:53 +0000 Subject: [PATCH 151/293] Bump ruff from 0.12.0 to 0.12.1 (#9241) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 96efee7020..1a0289a3ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.12.0 + rev: v0.12.1 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 89aba702b9..66b71c2225 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.3.7 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.12.0 # also change in .pre-commit-config.yaml when updating +ruff==0.12.1 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pre-commit From 30f61b26ff150e97b29adead8dae13e04d165510 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Sat, 28 Jun 2025 21:56:12 +0200 Subject: [PATCH 152/293] Remove backports of `std` (#9246) --- esphome/core/datatypes.h | 4 +- esphome/core/helpers.cpp | 15 ------ esphome/core/helpers.h | 109 ++++++--------------------------------- 3 files changed, 18 insertions(+), 110 deletions(-) diff --git a/esphome/core/datatypes.h b/esphome/core/datatypes.h index 5356be6b52..4929518387 100644 --- a/esphome/core/datatypes.h +++ b/esphome/core/datatypes.h @@ -11,7 +11,7 @@ namespace internal { /// Wrapper class for memory using big endian data layout, transparently converting it to native order. template class BigEndianLayout { public: - constexpr14 operator T() { return convert_big_endian(val_); } + constexpr operator T() { return convert_big_endian(val_); } private: T val_; @@ -20,7 +20,7 @@ template class BigEndianLayout { /// Wrapper class for memory using big endian data layout, transparently converting it to native order. template class LittleEndianLayout { public: - constexpr14 operator T() { return convert_little_endian(val_); } + constexpr operator T() { return convert_little_endian(val_); } private: T val_; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 79dbb314c8..fc91d83972 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -76,23 +76,8 @@ static const uint16_t CRC16_1021_BE_LUT_H[] = {0x0000, 0x1231, 0x2462, 0x3653, 0 0x9188, 0x83b9, 0xb5ea, 0xa7db, 0xd94c, 0xcb7d, 0xfd2e, 0xef1f}; #endif -// STL backports - -#if _GLIBCXX_RELEASE < 8 -std::string to_string(int value) { return str_snprintf("%d", 32, value); } // NOLINT -std::string to_string(long value) { return str_snprintf("%ld", 32, value); } // NOLINT -std::string to_string(long long value) { return str_snprintf("%lld", 32, value); } // NOLINT -std::string to_string(unsigned value) { return str_snprintf("%u", 32, value); } // NOLINT -std::string to_string(unsigned long value) { return str_snprintf("%lu", 32, value); } // NOLINT -std::string to_string(unsigned long long value) { return str_snprintf("%llu", 32, value); } // NOLINT -std::string to_string(float value) { return str_snprintf("%f", 32, value); } -std::string to_string(double value) { return str_snprintf("%f", 32, value); } -std::string to_string(long double value) { return str_snprintf("%Lf", 32, value); } -#endif - // Mathematics -float lerp(float completion, float start, float end) { return start + (end - start) * completion; } uint8_t crc8(const uint8_t *data, uint8_t len) { uint8_t crc = 0; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 8bd5b813c7..7d5366f323 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -37,89 +37,18 @@ #define ESPHOME_ALWAYS_INLINE __attribute__((always_inline)) #define PACKED __attribute__((packed)) -// Various functions can be constexpr in C++14, but not in C++11 (because their body isn't just a return statement). -// Define a substitute constexpr keyword for those functions, until we can drop C++11 support. -#if __cplusplus >= 201402L -#define constexpr14 constexpr -#else -#define constexpr14 inline // constexpr implies inline -#endif - namespace esphome { /// @name STL backports ///@{ -// Backports for various STL features we like to use. Pull in the STL implementation wherever available, to avoid -// ambiguity and to provide a uniform API. - -// std::to_string() from C++11, available from libstdc++/g++ 8 -// See https://github.com/espressif/esp-idf/issues/1445 -#if _GLIBCXX_RELEASE >= 8 +// Keep "using" even after the removal of our backports, to avoid breaking existing code. using std::to_string; -#else -std::string to_string(int value); // NOLINT -std::string to_string(long value); // NOLINT -std::string to_string(long long value); // NOLINT -std::string to_string(unsigned value); // NOLINT -std::string to_string(unsigned long value); // NOLINT -std::string to_string(unsigned long long value); // NOLINT -std::string to_string(float value); -std::string to_string(double value); -std::string to_string(long double value); -#endif - -// std::is_trivially_copyable from C++11, implemented in libstdc++/g++ 5.1 (but minor releases can't be detected) -#if _GLIBCXX_RELEASE >= 6 using std::is_trivially_copyable; -#else -// Implementing this is impossible without compiler intrinsics, so don't bother. Invalid usage will be detected on -// other variants that use a newer compiler anyway. -// NOLINTNEXTLINE(readability-identifier-naming) -template struct is_trivially_copyable : public std::integral_constant {}; -#endif - -// std::make_unique() from C++14 -#if __cpp_lib_make_unique >= 201304 using std::make_unique; -#else -template std::unique_ptr make_unique(Args &&...args) { - return std::unique_ptr(new T(std::forward(args)...)); -} -#endif - -// std::enable_if_t from C++14 -#if __cplusplus >= 201402L using std::enable_if_t; -#else -template using enable_if_t = typename std::enable_if::type; -#endif - -// std::clamp from C++17 -#if __cpp_lib_clamp >= 201603 using std::clamp; -#else -template constexpr const T &clamp(const T &v, const T &lo, const T &hi, Compare comp) { - return comp(v, lo) ? lo : comp(hi, v) ? hi : v; -} -template constexpr const T &clamp(const T &v, const T &lo, const T &hi) { - return clamp(v, lo, hi, std::less{}); -} -#endif - -// std::is_invocable from C++17 -#if __cpp_lib_is_invocable >= 201703 using std::is_invocable; -#else -// https://stackoverflow.com/a/37161919/8924614 -template struct is_invocable { // NOLINT(readability-identifier-naming) - template static auto test(U *p) -> decltype((*p)(std::declval()...), void(), std::true_type()); - template static auto test(...) -> decltype(std::false_type()); - static constexpr auto value = decltype(test(nullptr))::value; // NOLINT -}; -#endif - -// std::bit_cast from C++20 #if __cpp_lib_bit_cast >= 201806 using std::bit_cast; #else @@ -134,31 +63,29 @@ To bit_cast(const From &src) { return dst; } #endif +using std::lerp; // std::byteswap from C++23 -template constexpr14 T byteswap(T n) { +template constexpr T byteswap(T n) { T m; for (size_t i = 0; i < sizeof(T); i++) reinterpret_cast(&m)[i] = reinterpret_cast(&n)[sizeof(T) - 1 - i]; return m; } -template<> constexpr14 uint8_t byteswap(uint8_t n) { return n; } -template<> constexpr14 uint16_t byteswap(uint16_t n) { return __builtin_bswap16(n); } -template<> constexpr14 uint32_t byteswap(uint32_t n) { return __builtin_bswap32(n); } -template<> constexpr14 uint64_t byteswap(uint64_t n) { return __builtin_bswap64(n); } -template<> constexpr14 int8_t byteswap(int8_t n) { return n; } -template<> constexpr14 int16_t byteswap(int16_t n) { return __builtin_bswap16(n); } -template<> constexpr14 int32_t byteswap(int32_t n) { return __builtin_bswap32(n); } -template<> constexpr14 int64_t byteswap(int64_t n) { return __builtin_bswap64(n); } +template<> constexpr uint8_t byteswap(uint8_t n) { return n; } +template<> constexpr uint16_t byteswap(uint16_t n) { return __builtin_bswap16(n); } +template<> constexpr uint32_t byteswap(uint32_t n) { return __builtin_bswap32(n); } +template<> constexpr uint64_t byteswap(uint64_t n) { return __builtin_bswap64(n); } +template<> constexpr int8_t byteswap(int8_t n) { return n; } +template<> constexpr int16_t byteswap(int16_t n) { return __builtin_bswap16(n); } +template<> constexpr int32_t byteswap(int32_t n) { return __builtin_bswap32(n); } +template<> constexpr int64_t byteswap(int64_t n) { return __builtin_bswap64(n); } ///@} /// @name Mathematics ///@{ -/// Linearly interpolate between \p start and \p end by \p completion (between 0 and 1). -float lerp(float completion, float start, float end); - /// Remap \p value from the range (\p min, \p max) to (\p min_out, \p max_out). template T remap(U value, U min, U max, T min_out, T max_out) { return (value - min) * (max_out - min_out) / (max - min) + min_out; @@ -203,8 +130,7 @@ constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, ui } /// Encode a value from its constituent bytes (from most to least significant) in an array with length sizeof(T). -template::value, int> = 0> -constexpr14 T encode_value(const uint8_t *bytes) { +template::value, int> = 0> constexpr T encode_value(const uint8_t *bytes) { T val = 0; for (size_t i = 0; i < sizeof(T); i++) { val <<= 8; @@ -214,12 +140,12 @@ constexpr14 T encode_value(const uint8_t *bytes) { } /// Encode a value from its constituent bytes (from most to least significant) in an std::array with length sizeof(T). template::value, int> = 0> -constexpr14 T encode_value(const std::array bytes) { +constexpr T encode_value(const std::array bytes) { return encode_value(bytes.data()); } /// Decode a value into its constituent bytes (from most to least significant). template::value, int> = 0> -constexpr14 std::array decode_value(T val) { +constexpr std::array decode_value(T val) { std::array ret{}; for (size_t i = sizeof(T); i > 0; i--) { ret[i - 1] = val & 0xFF; @@ -246,7 +172,7 @@ inline uint32_t reverse_bits(uint32_t x) { } /// Convert a value between host byte order and big endian (most significant byte first) order. -template constexpr14 T convert_big_endian(T val) { +template constexpr T convert_big_endian(T val) { #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ return byteswap(val); #else @@ -255,7 +181,7 @@ template constexpr14 T convert_big_endian(T val) { } /// Convert a value between host byte order and little endian (least significant byte first) order. -template constexpr14 T convert_little_endian(T val) { +template constexpr T convert_little_endian(T val) { #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ return val; #else @@ -276,9 +202,6 @@ bool str_startswith(const std::string &str, const std::string &start); /// Check whether a string ends with a value. bool str_endswith(const std::string &str, const std::string &end); -/// Convert the value to a string (added as extra overload so that to_string() can be used on all stringifiable types). -inline std::string to_string(const std::string &val) { return val; } - /// Truncate a string to a specific length. std::string str_truncate(const std::string &str, size_t length); From 13d4823db6a29fe2ad09a0a08c3f20c18b6eff78 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Jun 2025 15:04:42 -0500 Subject: [PATCH 153/293] Fix buffer corruption in API message encoding with very verbose logging (#9249) --- esphome/components/api/api_connection.cpp | 35 ++++++-- esphome/components/api/api_connection.h | 8 ++ .../integration/fixtures/api_vv_logging.yaml | 89 +++++++++++++++++++ tests/integration/test_api_vv_logging.py | 83 +++++++++++++++++ 4 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 tests/integration/fixtures/api_vv_logging.yaml create mode 100644 tests/integration/test_api_vv_logging.py diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index fdcce6088c..65588ad4d8 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -94,6 +94,19 @@ APIConnection::~APIConnection() { #endif } +#ifdef HAS_PROTO_MESSAGE_DUMP +void APIConnection::log_batch_item_(const DeferredBatch::BatchItem &item) { + // Set log-only mode + this->log_only_mode_ = true; + + // Call the creator - it will create the message and log it via encode_message_to_buffer + item.creator(item.entity, this, std::numeric_limits::max(), true, item.message_type); + + // Clear log-only mode + this->log_only_mode_ = false; +} +#endif + void APIConnection::loop() { if (this->next_close_) { // requested a disconnect @@ -249,6 +262,14 @@ void APIConnection::on_disconnect_response(const DisconnectResponse &value) { // including header and footer overhead. Returns 0 if the message doesn't fit. uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { +#ifdef HAS_PROTO_MESSAGE_DUMP + // If in log-only mode, just log and return + if (conn->log_only_mode_) { + conn->log_send_message_(msg.message_name(), msg.dump()); + return 1; // Return non-zero to indicate "success" for logging + } +#endif + // Calculate size uint32_t calculated_size = 0; msg.calculate_size(calculated_size); @@ -276,11 +297,6 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes // Encode directly into buffer msg.encode(buffer); -#ifdef HAS_PROTO_MESSAGE_DUMP - // Log the message for VV debugging - conn->log_send_message_(msg.message_name(), msg.dump()); -#endif - // Calculate actual encoded size (not including header that was already added) size_t actual_payload_size = shared_buf.size() - size_before_encode; @@ -1891,6 +1907,15 @@ void APIConnection::process_batch_() { } } +#ifdef HAS_PROTO_MESSAGE_DUMP + // Log messages after send attempt for VV debugging + // It's safe to use the buffer for logging at this point regardless of send result + for (size_t i = 0; i < items_processed; i++) { + const auto &item = this->deferred_batch_.items[i]; + this->log_batch_item_(item); + } +#endif + // Handle remaining items more efficiently if (items_processed < this->deferred_batch_.items.size()) { // Remove processed items from the beginning diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index e872711e95..07e87ab39f 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -470,6 +470,10 @@ class APIConnection : public APIServerConnection { bool sent_ping_{false}; bool service_call_subscription_{false}; bool next_close_ = false; +#ifdef HAS_PROTO_MESSAGE_DUMP + // When true, encode_message_to_buffer will only log, not encode + bool log_only_mode_{false}; +#endif uint8_t ping_retries_{0}; // 8 bytes used, no padding needed @@ -627,6 +631,10 @@ class APIConnection : public APIServerConnection { // State for batch buffer allocation bool batch_first_message_{false}; +#ifdef HAS_PROTO_MESSAGE_DUMP + void log_batch_item_(const DeferredBatch::BatchItem &item); +#endif + // Helper function to schedule a deferred message with known message type bool schedule_message_(EntityBase *entity, MessageCreator creator, uint16_t message_type) { this->deferred_batch_.add_item(entity, std::move(creator), message_type); diff --git a/tests/integration/fixtures/api_vv_logging.yaml b/tests/integration/fixtures/api_vv_logging.yaml new file mode 100644 index 0000000000..df1edc796a --- /dev/null +++ b/tests/integration/fixtures/api_vv_logging.yaml @@ -0,0 +1,89 @@ +esphome: + name: vv-logging-test + +host: + +api: + +logger: + level: VERY_VERBOSE + # Enable VV logging for API components where the issue occurs + logs: + api.connection: VERY_VERBOSE + api.service: VERY_VERBOSE + api.proto: VERY_VERBOSE + sensor: VERY_VERBOSE + +# Create many sensors that update frequently to generate API traffic +# This will cause many messages to be batched and sent, triggering the +# code path where VV logging could cause buffer corruption +sensor: + - platform: template + name: "Test Sensor 1" + lambda: 'return millis() / 1000.0;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 2" + lambda: 'return (millis() / 1000.0) + 10;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 3" + lambda: 'return (millis() / 1000.0) + 20;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 4" + lambda: 'return (millis() / 1000.0) + 30;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 5" + lambda: 'return (millis() / 1000.0) + 40;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 6" + lambda: 'return (millis() / 1000.0) + 50;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 7" + lambda: 'return (millis() / 1000.0) + 60;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 8" + lambda: 'return (millis() / 1000.0) + 70;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 9" + lambda: 'return (millis() / 1000.0) + 80;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 10" + lambda: 'return (millis() / 1000.0) + 90;' + update_interval: 50ms + unit_of_measurement: "s" + +# Add some binary sensors too for variety +binary_sensor: + - platform: template + name: "Test Binary 1" + lambda: 'return (millis() / 1000) % 2 == 0;' + + - platform: template + name: "Test Binary 2" + lambda: 'return (millis() / 1000) % 3 == 0;' diff --git a/tests/integration/test_api_vv_logging.py b/tests/integration/test_api_vv_logging.py new file mode 100644 index 0000000000..19aab2001c --- /dev/null +++ b/tests/integration/test_api_vv_logging.py @@ -0,0 +1,83 @@ +"""Integration test for API with VERY_VERBOSE logging to verify no buffer corruption.""" + +from __future__ import annotations + +import asyncio +from typing import Any + +from aioesphomeapi import LogLevel +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_api_vv_logging( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that VERY_VERBOSE logging doesn't cause buffer corruption with API messages.""" + + # Track that we're receiving VV log messages and sensor updates + vv_logs_received = 0 + sensor_updates_received = 0 + errors_detected = [] + + def on_log(msg: Any) -> None: + """Capture log messages.""" + nonlocal vv_logs_received + # msg is a SubscribeLogsResponse object with 'message' attribute + # The message field is always bytes + message_text = msg.message.decode("utf-8", errors="replace") + + # Only count VV logs specifically + if "[VV]" in message_text: + vv_logs_received += 1 + + # Check for assertion or error messages + if "assert" in message_text.lower() or "error" in message_text.lower(): + errors_detected.append(message_text) + + # Write, compile and run the ESPHome device + async with run_compiled(yaml_config), api_client_connected() as client: + # Subscribe to VERY_VERBOSE logs - this enables the code path that could cause corruption + client.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_VERY_VERBOSE) + + # Wait for device to be ready + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "vv-logging-test" + + # Subscribe to sensor states + states = {} + + def on_state(state): + nonlocal sensor_updates_received + sensor_updates_received += 1 + states[state.key] = state + + client.subscribe_states(on_state) + + # List entities to find our test sensors + entity_info, _ = await client.list_entities_services() + + # Count sensors + sensor_count = sum(1 for e in entity_info if hasattr(e, "unit_of_measurement")) + assert sensor_count >= 10, f"Expected at least 10 sensors, got {sensor_count}" + + # Wait for sensor updates to flow with VV logging active + # The sensors update every 50ms, so we should get many updates + await asyncio.sleep(0.25) + + # Verify we received both VV logs and sensor updates + assert vv_logs_received > 0, "Expected to receive VERY_VERBOSE log messages" + assert sensor_updates_received > 10, ( + f"Expected many sensor updates, got {sensor_updates_received}" + ) + + # Check for any errors + if errors_detected: + pytest.fail(f"Errors detected during test: {errors_detected}") + + # The test passes if we didn't hit any assertions or buffer corruption From 3f1f99cf37cfc621a44ee69d339d2401a50f70a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Jun 2025 15:08:33 -0500 Subject: [PATCH 154/293] Extract lock-free queue and event pool to core helpers (#9238) --- esphome/components/esp32_ble/ble.cpp | 1 - esphome/components/esp32_ble/ble.h | 8 +- esphome/components/esp32_ble/ble_event.h | 12 +- esphome/components/esp32_ble/ble_event_pool.h | 72 ---------- esphome/components/esp32_ble/queue.h | 85 ----------- esphome/core/event_pool.h | 81 +++++++++++ esphome/core/lock_free_queue.h | 132 ++++++++++++++++++ 7 files changed, 223 insertions(+), 168 deletions(-) delete mode 100644 esphome/components/esp32_ble/ble_event_pool.h delete mode 100644 esphome/components/esp32_ble/queue.h create mode 100644 esphome/core/event_pool.h create mode 100644 esphome/core/lock_free_queue.h diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index b10d1fe10a..8b0cf4da98 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -1,7 +1,6 @@ #ifdef USE_ESP32 #include "ble.h" -#include "ble_event_pool.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 9fe996086e..ce452d65c4 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -12,8 +12,8 @@ #include "esphome/core/helpers.h" #include "ble_event.h" -#include "ble_event_pool.h" -#include "queue.h" +#include "esphome/core/lock_free_queue.h" +#include "esphome/core/event_pool.h" #ifdef USE_ESP32 @@ -148,8 +148,8 @@ class ESP32BLE : public Component { std::vector ble_status_event_handlers_; BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; - LockFreeQueue ble_events_; - BLEEventPool ble_event_pool_; + esphome::LockFreeQueue ble_events_; + esphome::EventPool ble_event_pool_; BLEAdvertising *advertising_{}; esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; uint32_t advertising_cycle_time_{}; diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index dd3ec3da42..9268c710f3 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -134,13 +134,13 @@ class BLEEvent { } // Destructor to clean up heap allocations - ~BLEEvent() { this->cleanup_heap_data(); } + ~BLEEvent() { this->release(); } // Default constructor for pre-allocation in pool BLEEvent() : type_(GAP) {} - // Clean up any heap-allocated data - void cleanup_heap_data() { + // Invoked on return to EventPool - clean up any heap-allocated data + void release() { if (this->type_ == GAP) { return; } @@ -161,19 +161,19 @@ class BLEEvent { // Load new event data for reuse (replaces previous event data) void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { - this->cleanup_heap_data(); + this->release(); this->type_ = GAP; this->init_gap_data_(e, p); } void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { - this->cleanup_heap_data(); + this->release(); this->type_ = GATTC; this->init_gattc_data_(e, i, p); } void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { - this->cleanup_heap_data(); + this->release(); this->type_ = GATTS; this->init_gatts_data_(e, i, p); } diff --git a/esphome/components/esp32_ble/ble_event_pool.h b/esphome/components/esp32_ble/ble_event_pool.h deleted file mode 100644 index ef123b1325..0000000000 --- a/esphome/components/esp32_ble/ble_event_pool.h +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#ifdef USE_ESP32 - -#include -#include -#include "ble_event.h" -#include "queue.h" -#include "esphome/core/helpers.h" - -namespace esphome { -namespace esp32_ble { - -// BLE Event Pool - On-demand pool of BLEEvent objects to avoid heap fragmentation -// Events are allocated on first use and reused thereafter, growing to peak usage -template class BLEEventPool { - public: - BLEEventPool() : total_created_(0) {} - - ~BLEEventPool() { - // Clean up any remaining events in the free list - BLEEvent *event; - while ((event = this->free_list_.pop()) != nullptr) { - delete event; - } - } - - // Allocate an event from the pool - // Returns nullptr if pool is full - BLEEvent *allocate() { - // Try to get from free list first - BLEEvent *event = this->free_list_.pop(); - if (event != nullptr) - return event; - - // Need to create a new event - if (this->total_created_ >= SIZE) { - // Pool is at capacity - return nullptr; - } - - // Use internal RAM for better performance - RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL); - event = allocator.allocate(1); - - if (event == nullptr) { - // Memory allocation failed - return nullptr; - } - - // Placement new to construct the object - new (event) BLEEvent(); - this->total_created_++; - return event; - } - - // Return an event to the pool for reuse - void release(BLEEvent *event) { - if (event != nullptr) { - this->free_list_.push(event); - } - } - - private: - LockFreeQueue free_list_; // Free events ready for reuse - uint8_t total_created_; // Total events created (high water mark) -}; - -} // namespace esp32_ble -} // namespace esphome - -#endif diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h deleted file mode 100644 index 75bf1eef25..0000000000 --- a/esphome/components/esp32_ble/queue.h +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once - -#ifdef USE_ESP32 - -#include -#include - -/* - * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather - * than using mutex-based locking, this lock-free queue allows the BLE - * task to enqueue events without blocking. The main loop() then processes - * these events at a safer time. - * - * This is a Single-Producer Single-Consumer (SPSC) lock-free ring buffer. - * The BLE task is the only producer, and the main loop() is the only consumer. - */ - -namespace esphome { -namespace esp32_ble { - -template class LockFreeQueue { - public: - LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {} - - bool push(T *element) { - if (element == nullptr) - return false; - - uint8_t current_tail = tail_.load(std::memory_order_relaxed); - uint8_t next_tail = (current_tail + 1) % SIZE; - - if (next_tail == head_.load(std::memory_order_acquire)) { - // Buffer full - dropped_count_.fetch_add(1, std::memory_order_relaxed); - return false; - } - - buffer_[current_tail] = element; - tail_.store(next_tail, std::memory_order_release); - return true; - } - - T *pop() { - uint8_t current_head = head_.load(std::memory_order_relaxed); - - if (current_head == tail_.load(std::memory_order_acquire)) { - return nullptr; // Empty - } - - T *element = buffer_[current_head]; - head_.store((current_head + 1) % SIZE, std::memory_order_release); - return element; - } - - size_t size() const { - uint8_t tail = tail_.load(std::memory_order_acquire); - uint8_t head = head_.load(std::memory_order_acquire); - return (tail - head + SIZE) % SIZE; - } - - uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } - - void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); } - - bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); } - - bool full() const { - uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; - return next_tail == head_.load(std::memory_order_acquire); - } - - protected: - T *buffer_[SIZE]; - // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) - std::atomic dropped_count_; // 65535 max - more than enough for drop tracking - // Atomic: written by consumer (pop), read by producer (push) to check if full - std::atomic head_; - // Atomic: written by producer (push), read by consumer (pop) to check if empty - std::atomic tail_; -}; - -} // namespace esp32_ble -} // namespace esphome - -#endif diff --git a/esphome/core/event_pool.h b/esphome/core/event_pool.h new file mode 100644 index 0000000000..69e03bafac --- /dev/null +++ b/esphome/core/event_pool.h @@ -0,0 +1,81 @@ +#pragma once + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + +#include +#include +#include "esphome/core/helpers.h" +#include "esphome/core/lock_free_queue.h" + +namespace esphome { + +// Event Pool - On-demand pool of objects to avoid heap fragmentation +// Events are allocated on first use and reused thereafter, growing to peak usage +// @tparam T The type of objects managed by the pool (must have a release() method) +// @tparam SIZE The maximum number of objects in the pool (1-255, limited by uint8_t) +template class EventPool { + public: + EventPool() : total_created_(0) {} + + ~EventPool() { + // Clean up any remaining events in the free list + // IMPORTANT: This destructor assumes no concurrent access. The EventPool must not + // be destroyed while any thread might still call allocate() or release(). + // In practice, this is typically ensured by destroying the pool only during + // component shutdown when all producer/consumer threads have been stopped. + T *event; + RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL); + while ((event = this->free_list_.pop()) != nullptr) { + // Call destructor + event->~T(); + // Deallocate using RAMAllocator + allocator.deallocate(event, 1); + } + } + + // Allocate an event from the pool + // Returns nullptr if pool is full + T *allocate() { + // Try to get from free list first + T *event = this->free_list_.pop(); + if (event != nullptr) + return event; + + // Need to create a new event + if (this->total_created_ >= SIZE) { + // Pool is at capacity + return nullptr; + } + + // Use internal RAM for better performance + RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL); + event = allocator.allocate(1); + + if (event == nullptr) { + // Memory allocation failed + return nullptr; + } + + // Placement new to construct the object + new (event) T(); + this->total_created_++; + return event; + } + + // Return an event to the pool for reuse + void release(T *event) { + if (event != nullptr) { + // Clean up the event's allocated memory + event->release(); + this->free_list_.push(event); + } + } + + private: + LockFreeQueue free_list_; // Free events ready for reuse + uint8_t total_created_; // Total events created (high water mark, max 255) +}; + +} // namespace esphome + +#endif // defined(USE_ESP32) || defined(USE_LIBRETINY) diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h new file mode 100644 index 0000000000..5460be0fae --- /dev/null +++ b/esphome/core/lock_free_queue.h @@ -0,0 +1,132 @@ +#pragma once + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + +#include +#include + +#if defined(USE_ESP32) +#include +#include +#elif defined(USE_LIBRETINY) +#include +#include +#endif + +/* + * Lock-free queue for single-producer single-consumer scenarios. + * This allows one thread to push items and another to pop them without + * blocking each other. + * + * This is a Single-Producer Single-Consumer (SPSC) lock-free ring buffer. + * Available on platforms with FreeRTOS support (ESP32, LibreTiny). + * + * Common use cases: + * - BLE events: BLE task produces, main loop consumes + * - MQTT messages: main task produces, MQTT thread consumes + * + * @tparam T The type of elements stored in the queue (must be a pointer type) + * @tparam SIZE The maximum number of elements (1-255, limited by uint8_t indices) + */ + +namespace esphome { + +template class LockFreeQueue { + public: + LockFreeQueue() : head_(0), tail_(0), dropped_count_(0), task_to_notify_(nullptr) {} + + bool push(T *element) { + if (element == nullptr) + return false; + + uint8_t current_tail = tail_.load(std::memory_order_relaxed); + uint8_t next_tail = (current_tail + 1) % SIZE; + + // Read head before incrementing tail + uint8_t head_before = head_.load(std::memory_order_acquire); + + if (next_tail == head_before) { + // Buffer full + dropped_count_.fetch_add(1, std::memory_order_relaxed); + return false; + } + + // Check if queue was empty before push + bool was_empty = (current_tail == head_before); + + buffer_[current_tail] = element; + tail_.store(next_tail, std::memory_order_release); + + // Notify optimization: only notify if we need to + if (task_to_notify_ != nullptr) { + if (was_empty) { + // Queue was empty - consumer might be going to sleep, must notify + xTaskNotifyGive(task_to_notify_); + } else { + // Queue wasn't empty - check if consumer has caught up to previous tail + uint8_t head_after = head_.load(std::memory_order_acquire); + if (head_after == current_tail) { + // Consumer just caught up to where tail was - might go to sleep, must notify + // Note: There's a benign race here - between reading head_after and calling + // xTaskNotifyGive(), the consumer could advance further. This would result + // in an unnecessary wake-up, but is harmless and extremely rare in practice. + xTaskNotifyGive(task_to_notify_); + } + // Otherwise: consumer is still behind, no need to notify + } + } + + return true; + } + + T *pop() { + uint8_t current_head = head_.load(std::memory_order_relaxed); + + if (current_head == tail_.load(std::memory_order_acquire)) { + return nullptr; // Empty + } + + T *element = buffer_[current_head]; + head_.store((current_head + 1) % SIZE, std::memory_order_release); + return element; + } + + size_t size() const { + uint8_t tail = tail_.load(std::memory_order_acquire); + uint8_t head = head_.load(std::memory_order_acquire); + return (tail - head + SIZE) % SIZE; + } + + uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } + + void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); } + + bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); } + + bool full() const { + uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; + return next_tail == head_.load(std::memory_order_acquire); + } + + // Set the FreeRTOS task handle to notify when items are pushed to the queue + // This enables efficient wake-up of a consumer task that's waiting for data + // @param task The FreeRTOS task handle to notify, or nullptr to disable notifications + void set_task_to_notify(TaskHandle_t task) { task_to_notify_ = task; } + + protected: + T *buffer_[SIZE]; + // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) + std::atomic dropped_count_; // 65535 max - more than enough for drop tracking + // Atomic: written by consumer (pop), read by producer (push) to check if full + // Using uint8_t limits queue size to 255 elements but saves memory and ensures + // atomic operations are efficient on all platforms + std::atomic head_; + // Atomic: written by producer (push), read by consumer (pop) to check if empty + std::atomic tail_; + // Task handle for notification (optional) + TaskHandle_t task_to_notify_; +}; + +} // namespace esphome + +#endif // defined(USE_ESP32) || defined(USE_LIBRETINY) From 86c0fb48a3dc1454cefc8cc31338300e2b971e51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Jun 2025 16:08:30 -0500 Subject: [PATCH 155/293] Replace ping retry timer with batch queue fallback (#9207) --- esphome/components/api/api_connection.cpp | 32 ++++++++++++----------- esphome/components/api/api_connection.h | 15 +++++++++-- esphome/components/api/api_server.cpp | 4 +-- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 65588ad4d8..f339a4b26f 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -65,10 +65,6 @@ uint32_t APIConnection::get_batch_delay_ms_() const { return this->parent_->get_ void APIConnection::start() { this->last_traffic_ = App.get_loop_component_start_time(); - // Set next_ping_retry_ to prevent immediate ping - // This ensures the first ping happens after the keepalive period - this->next_ping_retry_ = this->last_traffic_ + KEEPALIVE_TIMEOUT_MS; - APIError err = this->helper_->init(); if (err != APIError::OK) { on_fatal_error(); @@ -176,20 +172,15 @@ void APIConnection::loop() { on_fatal_error(); ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); } - } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && now > this->next_ping_retry_) { + } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) { ESP_LOGVV(TAG, "Sending keepalive PING"); this->sent_ping_ = this->send_message(PingRequest()); if (!this->sent_ping_) { - this->next_ping_retry_ = now + PING_RETRY_INTERVAL; - this->ping_retries_++; - if (this->ping_retries_ >= MAX_PING_RETRIES) { - on_fatal_error(); - ESP_LOGE(TAG, "%s: Ping failed %u times", this->get_client_combined_info().c_str(), this->ping_retries_); - } else if (this->ping_retries_ >= 10) { - ESP_LOGW(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_); - } else { - ESP_LOGD(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_); - } + // If we can't send the ping request directly (tx_buffer full), + // schedule it at the front of the batch so it will be sent with priority + ESP_LOGW(TAG, "Buffer full, ping queued"); + this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE); + this->sent_ping_ = true; // Mark as sent to avoid scheduling multiple pings } } @@ -1773,6 +1764,11 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c items.emplace_back(entity, std::move(creator), message_type); } +void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type) { + // Insert at front for high priority messages (no deduplication check) + items.insert(items.begin(), BatchItem(entity, std::move(creator), message_type)); +} + bool APIConnection::schedule_batch_() { if (!this->deferred_batch_.batch_scheduled) { this->deferred_batch_.batch_scheduled = true; @@ -1963,6 +1959,12 @@ uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConne return encode_message_to_buffer(req, DisconnectRequest::MESSAGE_TYPE, conn, remaining_size, is_single); } +uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, + bool is_single) { + PingRequest req; + return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single); +} + uint16_t APIConnection::get_estimated_message_size(uint16_t message_type) { // Use generated ESTIMATED_SIZE constants from each message type switch (message_type) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 07e87ab39f..4397462d8e 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -185,7 +185,6 @@ class APIConnection : public APIServerConnection { void on_disconnect_response(const DisconnectResponse &value) override; void on_ping_response(const PingResponse &value) override { // we initiated ping - this->ping_retries_ = 0; this->sent_ping_ = false; } void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; @@ -441,13 +440,16 @@ class APIConnection : public APIServerConnection { // Helper function to get estimated message size for buffer pre-allocation static uint16_t get_estimated_message_size(uint16_t message_type); + // Batch message method for ping requests + static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, + bool is_single); + // Pointers first (4 bytes each, naturally aligned) std::unique_ptr helper_; APIServer *parent_; // 4-byte aligned types uint32_t last_traffic_; - uint32_t next_ping_retry_{0}; int state_subs_at_ = -1; // Strings (12 bytes each on 32-bit) @@ -470,6 +472,7 @@ class APIConnection : public APIServerConnection { bool sent_ping_{false}; bool service_call_subscription_{false}; bool next_close_ = false; + // 7 bytes used, 1 byte padding #ifdef HAS_PROTO_MESSAGE_DUMP // When true, encode_message_to_buffer will only log, not encode bool log_only_mode_{false}; @@ -602,6 +605,8 @@ class APIConnection : public APIServerConnection { // Add item to the batch void add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type); + // Add item to the front of the batch (for high priority messages like ping) + void add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type); void clear() { items.clear(); batch_scheduled = false; @@ -645,6 +650,12 @@ class APIConnection : public APIServerConnection { bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) { return schedule_message_(entity, MessageCreator(function_ptr), message_type); } + + // Helper function to schedule a high priority message at the front of the batch + bool schedule_message_front_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) { + this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type); + return this->schedule_batch_(); + } }; } // namespace api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 583837af82..a33623b15a 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -526,8 +526,8 @@ void APIServer::on_shutdown() { for (auto &c : this->clients_) { if (!c->send_message(DisconnectRequest())) { // If we can't send the disconnect request directly (tx_buffer full), - // schedule it in the batch so it will be sent with the 5ms timer - c->schedule_message_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE); + // schedule it at the front of the batch so it will be sent with priority + c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE); } } } From 0c249a700610564d96a34896abaffc913b8a48c0 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 29 Jun 2025 06:16:34 -0500 Subject: [PATCH 156/293] [thermostat] Memory optimizations (#9259) --- .../thermostat/thermostat_climate.cpp | 65 ++++++++------- .../thermostat/thermostat_climate.h | 82 ++++++++++--------- 2 files changed, 77 insertions(+), 70 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index fe6ed8b159..404e585aff 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -997,7 +997,7 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { auto config = this->preset_config_.find(preset); if (config != this->preset_config_.end()) { - ESP_LOGI(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + ESP_LOGV(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); if (this->change_preset_internal_(config->second) || (!this->preset.has_value()) || this->preset.value() != preset) { // Fire any preset changed trigger if defined @@ -1015,7 +1015,7 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { this->custom_preset.reset(); this->preset = preset; } else { - ESP_LOGE(TAG, "Preset %s is not configured, ignoring.", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + ESP_LOGW(TAG, "Preset %s not configured; ignoring", LOG_STR_ARG(climate::climate_preset_to_string(preset))); } } @@ -1023,7 +1023,7 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) auto config = this->custom_preset_config_.find(custom_preset); if (config != this->custom_preset_config_.end()) { - ESP_LOGI(TAG, "Custom preset %s requested", custom_preset.c_str()); + ESP_LOGV(TAG, "Custom preset %s requested", custom_preset.c_str()); if (this->change_preset_internal_(config->second) || (!this->custom_preset.has_value()) || this->custom_preset.value() != custom_preset) { // Fire any preset changed trigger if defined @@ -1041,7 +1041,7 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) this->preset.reset(); this->custom_preset = custom_preset; } else { - ESP_LOGE(TAG, "Custom Preset %s is not configured, ignoring.", custom_preset.c_str()); + ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset.c_str()); } } @@ -1298,7 +1298,7 @@ void ThermostatClimate::dump_config() { if (this->supports_two_points_) { ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_); } - ESP_LOGCONFIG(TAG, " Start-up Delay Enabled: %s", YESNO(this->use_startup_delay_)); + ESP_LOGCONFIG(TAG, " Use Start-up Delay: %s", YESNO(this->use_startup_delay_)); if (this->supports_cool_) { ESP_LOGCONFIG(TAG, " Cooling Parameters:\n" @@ -1353,44 +1353,47 @@ void ThermostatClimate::dump_config() { } ESP_LOGCONFIG(TAG, " Minimum Idle Time: %" PRIu32 "s", this->timer_[thermostat::TIMER_IDLE_ON].time / 1000); ESP_LOGCONFIG(TAG, - " Supports AUTO: %s\n" - " Supports HEAT/COOL: %s\n" - " Supports COOL: %s\n" - " Supports DRY: %s\n" - " Supports FAN_ONLY: %s\n" - " Supports FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s\n" - " Supports FAN_ONLY_COOLING: %s", - YESNO(this->supports_auto_), YESNO(this->supports_heat_cool_), YESNO(this->supports_cool_), - YESNO(this->supports_dry_), YESNO(this->supports_fan_only_), + " Supported MODES:\n" + " AUTO: %s\n" + " HEAT/COOL: %s\n" + " HEAT: %s\n" + " COOL: %s\n" + " DRY: %s\n" + " FAN_ONLY: %s\n" + " FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s\n" + " FAN_ONLY_COOLING: %s", + YESNO(this->supports_auto_), YESNO(this->supports_heat_cool_), YESNO(this->supports_heat_), + YESNO(this->supports_cool_), YESNO(this->supports_dry_), YESNO(this->supports_fan_only_), YESNO(this->supports_fan_only_action_uses_fan_mode_timer_), YESNO(this->supports_fan_only_cooling_)); if (this->supports_cool_) { - ESP_LOGCONFIG(TAG, " Supports FAN_WITH_COOLING: %s", YESNO(this->supports_fan_with_cooling_)); + ESP_LOGCONFIG(TAG, " FAN_WITH_COOLING: %s", YESNO(this->supports_fan_with_cooling_)); } if (this->supports_heat_) { - ESP_LOGCONFIG(TAG, " Supports FAN_WITH_HEATING: %s", YESNO(this->supports_fan_with_heating_)); + ESP_LOGCONFIG(TAG, " FAN_WITH_HEATING: %s", YESNO(this->supports_fan_with_heating_)); } - ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); ESP_LOGCONFIG(TAG, - " Supports FAN MODE ON: %s\n" - " Supports FAN MODE OFF: %s\n" - " Supports FAN MODE AUTO: %s\n" - " Supports FAN MODE LOW: %s\n" - " Supports FAN MODE MEDIUM: %s\n" - " Supports FAN MODE HIGH: %s\n" - " Supports FAN MODE MIDDLE: %s\n" - " Supports FAN MODE FOCUS: %s\n" - " Supports FAN MODE DIFFUSE: %s\n" - " Supports FAN MODE QUIET: %s", + " Supported FAN MODES:\n" + " ON: %s\n" + " OFF: %s\n" + " AUTO: %s\n" + " LOW: %s\n" + " MEDIUM: %s\n" + " HIGH: %s\n" + " MIDDLE: %s\n" + " FOCUS: %s\n" + " DIFFUSE: %s\n" + " QUIET: %s", YESNO(this->supports_fan_mode_on_), YESNO(this->supports_fan_mode_off_), YESNO(this->supports_fan_mode_auto_), YESNO(this->supports_fan_mode_low_), YESNO(this->supports_fan_mode_medium_), YESNO(this->supports_fan_mode_high_), YESNO(this->supports_fan_mode_middle_), YESNO(this->supports_fan_mode_focus_), YESNO(this->supports_fan_mode_diffuse_), YESNO(this->supports_fan_mode_quiet_)); ESP_LOGCONFIG(TAG, - " Supports SWING MODE BOTH: %s\n" - " Supports SWING MODE OFF: %s\n" - " Supports SWING MODE HORIZONTAL: %s\n" - " Supports SWING MODE VERTICAL: %s\n" + " Supported SWING MODES:\n" + " BOTH: %s\n" + " OFF: %s\n" + " HORIZONTAL: %s\n" + " VERTICAL: %s\n" " Supports TWO SET POINTS: %s", YESNO(this->supports_swing_mode_both_), YESNO(this->supports_swing_mode_off_), YESNO(this->supports_swing_mode_horizontal_), YESNO(this->supports_swing_mode_vertical_), diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 50510cf070..007d7297d5 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -13,7 +13,7 @@ namespace esphome { namespace thermostat { -enum ThermostatClimateTimerIndex : size_t { +enum ThermostatClimateTimerIndex : uint8_t { TIMER_COOLING_MAX_RUN_TIME = 0, TIMER_COOLING_OFF = 1, TIMER_COOLING_ON = 2, @@ -26,7 +26,11 @@ enum ThermostatClimateTimerIndex : size_t { TIMER_IDLE_ON = 9, }; -enum OnBootRestoreFrom : size_t { MEMORY = 0, DEFAULT_PRESET = 1 }; +enum OnBootRestoreFrom : uint8_t { + MEMORY = 0, + DEFAULT_PRESET = 1, +}; + struct ThermostatClimateTimer { bool active; uint32_t time; @@ -65,7 +69,7 @@ class ThermostatClimate : public climate::Climate, public Component { void set_default_preset(const std::string &custom_preset); void set_default_preset(climate::ClimatePreset preset); - void set_on_boot_restore_from(thermostat::OnBootRestoreFrom on_boot_restore_from); + void set_on_boot_restore_from(OnBootRestoreFrom on_boot_restore_from); void set_set_point_minimum_differential(float differential); void set_cool_deadband(float deadband); void set_cool_overrun(float overrun); @@ -240,10 +244,8 @@ class ThermostatClimate : public climate::Climate, public Component { void dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config, bool is_default_preset); - /// The sensor used for getting the current temperature - sensor::Sensor *sensor_{nullptr}; - /// The sensor used for getting the current humidity - sensor::Sensor *humidity_sensor_{nullptr}; + /// Minimum allowable duration in seconds for action timers + const uint8_t min_timer_duration_{1}; /// Whether the controller supports auto/cooling/drying/fanning/heating. /// @@ -310,6 +312,31 @@ class ThermostatClimate : public climate::Climate, public Component { /// setup_complete_ blocks modifying/resetting the temps immediately after boot bool setup_complete_{false}; + /// Store previously-known temperatures + /// + /// These are used to determine when the temperature change trigger/action needs to be called + float prev_target_temperature_{NAN}; + float prev_target_temperature_low_{NAN}; + float prev_target_temperature_high_{NAN}; + + /// Minimum differential required between set points + float set_point_minimum_differential_{0}; + + /// Hysteresis values used for computing climate actions + float cooling_deadband_{0}; + float cooling_overrun_{0}; + float heating_deadband_{0}; + float heating_overrun_{0}; + + /// Maximum allowable temperature deltas before engaging supplemental cooling/heating actions + float supplemental_cool_delta_{0}; + float supplemental_heat_delta_{0}; + + /// The sensor used for getting the current temperature + sensor::Sensor *sensor_{nullptr}; + /// The sensor used for getting the current humidity + sensor::Sensor *humidity_sensor_{nullptr}; + /// The trigger to call when the controller should switch to cooling action/mode. /// /// A null value for this attribute means that the controller has no cooling action @@ -399,7 +426,7 @@ class ThermostatClimate : public climate::Climate, public Component { /// The trigger to call when the target temperature(s) change(es). Trigger<> *temperature_change_trigger_{nullptr}; - /// The triggr to call when the preset mode changes + /// The trigger to call when the preset mode changes Trigger<> *preset_change_trigger_{nullptr}; /// A reference to the trigger that was previously active. @@ -411,6 +438,10 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *prev_mode_trigger_{nullptr}; Trigger<> *prev_swing_mode_trigger_{nullptr}; + /// If set to DEFAULT_PRESET then the default preset is always used. When MEMORY prior + /// state will attempt to be restored if possible + OnBootRestoreFrom on_boot_restore_from_{OnBootRestoreFrom::MEMORY}; + /// Store previously-known states /// /// These are used to determine when a trigger/action needs to be called @@ -419,28 +450,10 @@ class ThermostatClimate : public climate::Climate, public Component { climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; - /// Store previously-known temperatures - /// - /// These are used to determine when the temperature change trigger/action needs to be called - float prev_target_temperature_{NAN}; - float prev_target_temperature_low_{NAN}; - float prev_target_temperature_high_{NAN}; - - /// Minimum differential required between set points - float set_point_minimum_differential_{0}; - - /// Hysteresis values used for computing climate actions - float cooling_deadband_{0}; - float cooling_overrun_{0}; - float heating_deadband_{0}; - float heating_overrun_{0}; - - /// Maximum allowable temperature deltas before engauging supplemental cooling/heating actions - float supplemental_cool_delta_{0}; - float supplemental_heat_delta_{0}; - - /// Minimum allowable duration in seconds for action timers - const uint8_t min_timer_duration_{1}; + /// Default standard preset to use on start up + climate::ClimatePreset default_preset_{}; + /// Default custom preset to use on start up + std::string default_custom_preset_{}; /// Climate action timers std::vector timer_{ @@ -460,15 +473,6 @@ class ThermostatClimate : public climate::Climate, public Component { std::map preset_config_{}; /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") std::map custom_preset_config_{}; - - /// Default standard preset to use on start up - climate::ClimatePreset default_preset_{}; - /// Default custom preset to use on start up - std::string default_custom_preset_{}; - - /// If set to DEFAULT_PRESET then the default preset is always used. When MEMORY prior - /// state will attempt to be restored if possible - thermostat::OnBootRestoreFrom on_boot_restore_from_{thermostat::OnBootRestoreFrom::MEMORY}; }; } // namespace thermostat From 53ab016098be0719f182c3586b65d47e0f54a85c Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 29 Jun 2025 06:17:53 -0500 Subject: [PATCH 157/293] [adc] Memory optimizations (#9247) --- esphome/components/adc/adc_sensor.h | 15 ++++++++---- esphome/components/adc/adc_sensor_common.cpp | 2 +- esphome/components/adc/adc_sensor_esp32.cpp | 24 +++++++++++++------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 62f2461245..9ffb6cf856 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -28,19 +28,24 @@ static const adc_atten_t ADC_ATTEN_DB_12_COMPAT = ADC_ATTEN_DB_11; #endif #endif // USE_ESP32 -enum class SamplingMode : uint8_t { AVG = 0, MIN = 1, MAX = 2 }; +enum class SamplingMode : uint8_t { + AVG = 0, + MIN = 1, + MAX = 2, +}; + const LogString *sampling_mode_to_str(SamplingMode mode); class Aggregator { public: + Aggregator(SamplingMode mode); void add_sample(uint32_t value); uint32_t aggregate(); - Aggregator(SamplingMode mode); protected: - SamplingMode mode_{SamplingMode::AVG}; uint32_t aggr_{0}; uint32_t samples_{0}; + SamplingMode mode_{SamplingMode::AVG}; }; class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { @@ -81,9 +86,9 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage #endif // USE_RP2040 protected: - InternalGPIOPin *pin_; - bool output_raw_{false}; uint8_t sample_count_{1}; + bool output_raw_{false}; + InternalGPIOPin *pin_; SamplingMode sampling_mode_{SamplingMode::AVG}; #ifdef USE_RP2040 diff --git a/esphome/components/adc/adc_sensor_common.cpp b/esphome/components/adc/adc_sensor_common.cpp index c7509c7c7a..797ab75045 100644 --- a/esphome/components/adc/adc_sensor_common.cpp +++ b/esphome/components/adc/adc_sensor_common.cpp @@ -61,7 +61,7 @@ uint32_t Aggregator::aggregate() { void ADCSensor::update() { float value_v = this->sample(); - ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v); + ESP_LOGV(TAG, "'%s': Voltage=%.4fV", this->get_name().c_str(), value_v); this->publish_state(value_v); } diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index d6cf6e893b..ed1f3329ab 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -55,32 +55,40 @@ void ADCSensor::setup() { } void ADCSensor::dump_config() { + static const char *const ATTEN_AUTO_STR = "auto"; + static const char *const ATTEN_0DB_STR = "0 db"; + static const char *const ATTEN_2_5DB_STR = "2.5 db"; + static const char *const ATTEN_6DB_STR = "6 db"; + static const char *const ATTEN_12DB_STR = "12 db"; + const char *atten_str = ATTEN_AUTO_STR; + LOG_SENSOR("", "ADC Sensor", this); LOG_PIN(" Pin: ", this->pin_); - if (this->autorange_) { - ESP_LOGCONFIG(TAG, " Attenuation: auto"); - } else { + + if (!this->autorange_) { switch (this->attenuation_) { case ADC_ATTEN_DB_0: - ESP_LOGCONFIG(TAG, " Attenuation: 0db"); + atten_str = ATTEN_0DB_STR; break; case ADC_ATTEN_DB_2_5: - ESP_LOGCONFIG(TAG, " Attenuation: 2.5db"); + atten_str = ATTEN_2_5DB_STR; break; case ADC_ATTEN_DB_6: - ESP_LOGCONFIG(TAG, " Attenuation: 6db"); + atten_str = ATTEN_6DB_STR; break; case ADC_ATTEN_DB_12_COMPAT: - ESP_LOGCONFIG(TAG, " Attenuation: 12db"); + atten_str = ATTEN_12DB_STR; break; default: // This is to satisfy the unused ADC_ATTEN_MAX break; } } + ESP_LOGCONFIG(TAG, + " Attenuation: %s\n" " Samples: %i\n" " Sampling mode: %s", - this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); + atten_str, this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); LOG_UPDATE_INTERVAL(this); } From 21e1f3d1034098e27f9d793a08436955486f7e40 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 29 Jun 2025 06:28:51 -0500 Subject: [PATCH 158/293] [light] Memory optimizations (#9260) --- .../components/light/esp_color_correction.h | 2 +- esphome/components/light/light_call.cpp | 44 +++++++++---------- esphome/components/light/light_state.h | 9 ++-- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index 979a1acb07..39ce5700c6 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -69,8 +69,8 @@ class ESPColorCorrection { protected: uint8_t gamma_table_[256]; uint8_t gamma_reverse_table_[256]; - Color max_brightness_; uint8_t local_brightness_{255}; + Color max_brightness_; }; } // namespace light diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index c2600d05c2..78b0ac9feb 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -136,7 +136,7 @@ LightColorValues LightCall::validate_() { // Color mode check if (this->color_mode_.has_value() && !traits.supports_color_mode(this->color_mode_.value())) { - ESP_LOGW(TAG, "'%s' - This light does not support color mode %s!", name, + ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_.value()))); this->color_mode_.reset(); } @@ -152,20 +152,20 @@ LightColorValues LightCall::validate_() { // Brightness exists check if (this->brightness_.has_value() && *this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) { - ESP_LOGW(TAG, "'%s' - This light does not support setting brightness!", name); + ESP_LOGW(TAG, "'%s': setting brightness not supported", name); this->brightness_.reset(); } // Transition length possible check if (this->transition_length_.has_value() && *this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) { - ESP_LOGW(TAG, "'%s' - This light does not support transitions!", name); + ESP_LOGW(TAG, "'%s': transitions not supported", name); this->transition_length_.reset(); } // Color brightness exists check if (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting RGB brightness!", name); + ESP_LOGW(TAG, "'%s': color mode does not support setting RGB brightness", name); this->color_brightness_.reset(); } @@ -173,7 +173,7 @@ LightColorValues LightCall::validate_() { if ((this->red_.has_value() && *this->red_ > 0.0f) || (this->green_.has_value() && *this->green_ > 0.0f) || (this->blue_.has_value() && *this->blue_ > 0.0f)) { if (!(color_mode & ColorCapability::RGB)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting RGB color!", name); + ESP_LOGW(TAG, "'%s': color mode does not support setting RGB color", name); this->red_.reset(); this->green_.reset(); this->blue_.reset(); @@ -183,14 +183,14 @@ LightColorValues LightCall::validate_() { // White value exists check if (this->white_.has_value() && *this->white_ > 0.0f && !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting white value!", name); + ESP_LOGW(TAG, "'%s': color mode does not support setting white value", name); this->white_.reset(); } // Color temperature exists check if (this->color_temperature_.has_value() && !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting color temperature!", name); + ESP_LOGW(TAG, "'%s': color mode does not support setting color temperature", name); this->color_temperature_.reset(); } @@ -198,7 +198,7 @@ LightColorValues LightCall::validate_() { if ((this->cold_white_.has_value() && *this->cold_white_ > 0.0f) || (this->warm_white_.has_value() && *this->warm_white_ > 0.0f)) { if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting cold/warm white value!", name); + ESP_LOGW(TAG, "'%s': color mode does not support setting cold/warm white value", name); this->cold_white_.reset(); this->warm_white_.reset(); } @@ -208,7 +208,7 @@ LightColorValues LightCall::validate_() { if (name_##_.has_value()) { \ auto val = *name_##_; \ if (val < (min) || val > (max)) { \ - ESP_LOGW(TAG, "'%s' - %s value %.2f is out of range [%.1f - %.1f]!", name, LOG_STR_LITERAL(upper_name), val, \ + ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_LITERAL(upper_name), val, \ (min), (max)); \ name_##_ = clamp(val, (min), (max)); \ } \ @@ -270,7 +270,7 @@ LightColorValues LightCall::validate_() { // Flash length check if (this->has_flash_() && *this->flash_length_ == 0) { - ESP_LOGW(TAG, "'%s' - Flash length must be greater than zero!", name); + ESP_LOGW(TAG, "'%s': flash length must be greater than zero", name); this->flash_length_.reset(); } @@ -284,18 +284,18 @@ LightColorValues LightCall::validate_() { // validate effect index if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) { - ESP_LOGW(TAG, "'%s' - Invalid effect index %" PRIu32 "!", name, *this->effect_); + ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, *this->effect_); this->effect_.reset(); } if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { - ESP_LOGW(TAG, "'%s' - Effect cannot be used together with transition/flash!", name); + ESP_LOGW(TAG, "'%s': effect cannot be used with transition/flash", name); this->transition_length_.reset(); this->flash_length_.reset(); } if (this->has_flash_() && this->has_transition_()) { - ESP_LOGW(TAG, "'%s' - Flash cannot be used together with transition!", name); + ESP_LOGW(TAG, "'%s': flash cannot be used with transition", name); this->transition_length_.reset(); } @@ -311,7 +311,7 @@ LightColorValues LightCall::validate_() { } if (this->has_transition_() && !supports_transition) { - ESP_LOGW(TAG, "'%s' - Light does not support transitions!", name); + ESP_LOGW(TAG, "'%s': transitions not supported", name); this->transition_length_.reset(); } @@ -320,7 +320,7 @@ LightColorValues LightCall::validate_() { // Reason: When user turns off the light in frontend, the effect should also stop if (!this->has_flash_() && !this->state_.value_or(v.is_on())) { if (this->has_effect_()) { - ESP_LOGW(TAG, "'%s' - Cannot start an effect when turning off!", name); + ESP_LOGW(TAG, "'%s': cannot start effect when turning off", name); this->effect_.reset(); } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) { // Auto turn off effect @@ -348,7 +348,7 @@ void LightCall::transform_parameters_() { !(*this->color_mode_ & ColorCapability::WHITE) && // !(*this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && // traits.get_min_mireds() > 0.0f && traits.get_max_mireds() > 0.0f) { - ESP_LOGD(TAG, "'%s' - Setting cold/warm white channels using white/color temperature values.", + ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values", this->parent_->get_name().c_str()); if (this->color_temperature_.has_value()) { const float color_temp = clamp(*this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds()); @@ -388,8 +388,8 @@ ColorMode LightCall::compute_color_mode_() { // Don't change if the current mode is suitable. if (suitable_modes.count(current_mode) > 0) { - ESP_LOGI(TAG, "'%s' - Keeping current color mode %s for call without color mode.", - this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(current_mode))); + ESP_LOGI(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(), + LOG_STR_ARG(color_mode_to_human(current_mode))); return current_mode; } @@ -398,7 +398,7 @@ ColorMode LightCall::compute_color_mode_() { if (supported_modes.count(mode) == 0) continue; - ESP_LOGI(TAG, "'%s' - Using color mode %s for call without color mode.", this->parent_->get_name().c_str(), + ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(mode))); return mode; } @@ -406,8 +406,8 @@ ColorMode LightCall::compute_color_mode_() { // There's no supported mode for this call, so warn, use the current more or a mode at random and let validation strip // out whatever we don't support. auto color_mode = current_mode != ColorMode::UNKNOWN ? current_mode : *supported_modes.begin(); - ESP_LOGW(TAG, "'%s' - No color mode suitable for this call supported, defaulting to %s!", - this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(color_mode))); + ESP_LOGW(TAG, "'%s': no suitable color mode supported; defaulting to %s", this->parent_->get_name().c_str(), + LOG_STR_ARG(color_mode_to_human(color_mode))); return color_mode; } std::set LightCall::get_suitable_color_modes_() { @@ -472,7 +472,7 @@ LightCall &LightCall::set_effect(const std::string &effect) { } } if (!found) { - ESP_LOGW(TAG, "'%s' - No such effect '%s'", this->parent_->get_name().c_str(), effect.c_str()); + ESP_LOGW(TAG, "'%s': no such effect '%s'", this->parent_->get_name().c_str(), effect.c_str()); } return *this; } diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index b93823feac..f21fb8a06e 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -225,6 +225,11 @@ class LightState : public EntityBase, public Component { /// Gamma correction factor for the light. float gamma_correct_{}; + /// Whether the light value should be written in the next cycle. + bool next_write_{true}; + // for effects, true if a transformer (transition) is active. + bool is_transformer_active_ = false; + /// Object used to store the persisted values of the light. ESPPreferenceObject rtc_; @@ -247,10 +252,6 @@ class LightState : public EntityBase, public Component { /// Restore mode of the light. LightRestoreMode restore_mode_; - /// Whether the light value should be written in the next cycle. - bool next_write_{true}; - // for effects, true if a transformer (transition) is active. - bool is_transformer_active_ = false; }; } // namespace light From 921d0888cdce6e94a29c024c604812e54aa59d40 Mon Sep 17 00:00:00 2001 From: Rezoran <30475103+Rezoran@users.noreply.github.com> Date: Sun, 29 Jun 2025 17:05:23 +0200 Subject: [PATCH 159/293] [uart] fix: missing uart_config_t struct initialisation (#9235) --- esphome/components/uart/uart_component_esp_idf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 84bd48d530..8fae63a603 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -42,7 +42,7 @@ uart_config_t IDFUARTComponent::get_config_() { break; } - uart_config_t uart_config; + uart_config_t uart_config{}; uart_config.baud_rate = this->baud_rate_; uart_config.data_bits = data_bits; uart_config.parity = parity; From ddbcf8549c21c68a48aade8824a22f55c055f03e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 13:29:18 -0500 Subject: [PATCH 160/293] Reduce web_server code duplication by extracting detail parameter parsing (#9257) --- esphome/components/web_server/web_server.cpp | 126 ++++--------------- 1 file changed, 26 insertions(+), 100 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index becb5bc2c7..9f42253794 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -370,6 +370,12 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { set_json_value(root, obj, sensor, value, start_config); \ (root)["state"] = state; +// Helper to get request detail parameter +static JsonDetail get_request_detail(AsyncWebServerRequest *request) { + auto *param = request->getParam("detail"); + return (param && param->value() == "all") ? DETAIL_ALL : DETAIL_STATE; +} + #ifdef USE_SENSOR void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { if (this->events_.empty()) @@ -381,11 +387,7 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -435,11 +437,7 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->text_sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -483,11 +481,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->switch_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { @@ -534,11 +528,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->button_json(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "press") { @@ -584,11 +574,7 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->binary_sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -632,11 +618,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->fan_json(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { @@ -722,11 +704,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->light_json(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { @@ -847,11 +825,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->cover_json(obj, detail); request->send(200, "application/json", data.c_str()); return; @@ -937,11 +911,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->number_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -1016,11 +986,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->date_json(obj, detail); request->send(200, "application/json", data.c_str()); return; @@ -1084,11 +1050,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->time_json(obj, detail); request->send(200, "application/json", data.c_str()); return; @@ -1151,11 +1113,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->datetime_json(obj, detail); request->send(200, "application/json", data.c_str()); return; @@ -1220,11 +1178,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->text_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -1290,11 +1244,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->select_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -1358,11 +1308,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->climate_json(obj, detail); request->send(200, "application/json", data.c_str()); return; @@ -1526,11 +1472,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->lock_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "lock") { @@ -1583,11 +1525,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->valve_json(obj, detail); request->send(200, "application/json", data.c_str()); return; @@ -1664,11 +1602,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->alarm_control_panel_json(obj, obj->get_state(), detail); request->send(200, "application/json", data.c_str()); return; @@ -1740,11 +1674,7 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->event_json(obj, "", detail); request->send(200, "application/json", data.c_str()); return; @@ -1795,11 +1725,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->update_json(obj, detail); request->send(200, "application/json", data.c_str()); return; From ed9850c4a4937b0276cb0c2565a4ac81caf387b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 13:46:28 -0500 Subject: [PATCH 161/293] Remove redundant get_setup_priority() overrides returning default value (#9253) --- esphome/components/ade7880/ade7880.h | 2 -- esphome/components/ads1115/ads1115.h | 1 - esphome/components/ads1118/ads1118.h | 1 - esphome/components/ags10/ags10.h | 2 -- esphome/components/aic3204/aic3204.h | 1 - esphome/components/alpha3/alpha3.h | 1 - esphome/components/am43/cover/am43_cover.h | 1 - esphome/components/am43/sensor/am43_sensor.h | 1 - .../analog_threshold/analog_threshold_binary_sensor.h | 2 -- esphome/components/anova/anova.h | 1 - esphome/components/as5600/as5600.h | 1 - esphome/components/atc_mithermometer/atc_mithermometer.h | 1 - esphome/components/b_parasite/b_parasite.h | 1 - esphome/components/ble_client/output/ble_binary_output.h | 1 - esphome/components/ble_client/sensor/ble_rssi_sensor.h | 1 - esphome/components/ble_client/sensor/ble_sensor.h | 1 - esphome/components/ble_client/switch/ble_switch.h | 1 - esphome/components/ble_client/text_sensor/ble_text_sensor.h | 1 - esphome/components/ble_presence/ble_presence_device.h | 1 - esphome/components/ble_rssi/ble_rssi_sensor.h | 1 - esphome/components/ble_scanner/ble_scanner.h | 1 - esphome/components/bmp581/bmp581.h | 2 -- esphome/components/cap1188/cap1188.h | 1 - esphome/components/ccs811/ccs811.h | 2 -- esphome/components/copy/binary_sensor/copy_binary_sensor.h | 1 - esphome/components/copy/button/copy_button.h | 1 - esphome/components/copy/cover/copy_cover.h | 1 - esphome/components/copy/fan/copy_fan.h | 1 - esphome/components/copy/lock/copy_lock.h | 1 - esphome/components/copy/number/copy_number.h | 1 - esphome/components/copy/select/copy_select.h | 1 - esphome/components/copy/sensor/copy_sensor.h | 1 - esphome/components/copy/switch/copy_switch.h | 1 - esphome/components/copy/text/copy_text.h | 1 - esphome/components/copy/text_sensor/copy_text_sensor.h | 1 - esphome/components/cs5460a/cs5460a.h | 1 - esphome/components/duty_time/duty_time_sensor.h | 1 - esphome/components/ens160_base/ens160_base.h | 1 - esphome/components/es7210/es7210.h | 1 - esphome/components/es7243e/es7243e.h | 1 - esphome/components/es8156/es8156.h | 1 - esphome/components/es8311/es8311.h | 1 - esphome/components/es8388/es8388.h | 1 - esphome/components/esp32_touch/esp32_touch.h | 1 - esphome/components/ezo/ezo.h | 1 - esphome/components/ezo_pmp/ezo_pmp.h | 1 - esphome/components/feedback/feedback_cover.h | 1 - esphome/components/fs3000/fs3000.h | 1 - esphome/components/gcja5/gcja5.h | 1 - esphome/components/gp8403/gp8403.h | 1 - esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h | 2 -- esphome/components/he60r/he60r.h | 1 - esphome/components/honeywellabp2_i2c/honeywellabp2.h | 1 - esphome/components/i2c_device/i2c_device.h | 1 - esphome/components/iaqcore/iaqcore.h | 2 -- esphome/components/ina260/ina260.h | 2 -- esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h | 1 - esphome/components/integration/integration_sensor.h | 1 - esphome/components/interval/interval.h | 2 -- esphome/components/ltr390/ltr390.h | 1 - esphome/components/ltr501/ltr501.h | 1 - esphome/components/ltr_als_ps/ltr_als_ps.h | 1 - esphome/components/max9611/max9611.h | 1 - esphome/components/mcp9600/mcp9600.h | 2 -- esphome/components/mopeka_pro_check/mopeka_pro_check.h | 1 - esphome/components/mopeka_std_check/mopeka_std_check.h | 1 - esphome/components/mpl3115a2/mpl3115a2.h | 2 -- esphome/components/ms8607/ms8607.h | 1 - esphome/components/pmsa003i/pmsa003i.h | 1 - esphome/components/pmsx003/pmsx003.h | 1 - esphome/components/pn7150/pn7150.h | 1 - esphome/components/pn7160/pn7160.h | 1 - esphome/components/pulse_counter/pulse_counter_sensor.h | 1 - esphome/components/pulse_width/pulse_width.h | 1 - esphome/components/pvvx_mithermometer/display/pvvx_display.h | 2 -- esphome/components/pvvx_mithermometer/pvvx_mithermometer.h | 1 - esphome/components/qwiic_pir/qwiic_pir.h | 1 - esphome/components/rc522/rc522.h | 1 - esphome/components/rdm6300/rdm6300.h | 2 -- esphome/components/remote_receiver/remote_receiver.h | 1 - esphome/components/resistance/resistance_sensor.h | 1 - esphome/components/ruuvitag/ruuvitag.h | 1 - esphome/components/scd30/scd30.h | 1 - esphome/components/scd4x/scd4x.h | 1 - esphome/components/script/script.h | 2 -- esphome/components/sen5x/sen5x.h | 1 - esphome/components/senseair/senseair.h | 1 - esphome/components/servo/servo.h | 1 - esphome/components/sfa30/sfa30.h | 1 - esphome/components/sgp30/sgp30.h | 1 - esphome/components/sgp4x/sgp4x.h | 1 - esphome/components/sht4x/sht4x.h | 1 - esphome/components/sm300d2/sm300d2.h | 2 -- esphome/components/sps30/sps30.h | 1 - esphome/components/status/status_binary_sensor.h | 2 -- esphome/components/switch/binary_sensor/switch_binary_sensor.h | 1 - esphome/components/tmp1075/tmp1075.h | 2 -- esphome/components/tof10120/tof10120_sensor.h | 1 - esphome/components/tormatic/tormatic_cover.h | 1 - esphome/components/total_daily_energy/total_daily_energy.h | 1 - esphome/components/ttp229_bsf/ttp229_bsf.h | 1 - esphome/components/ttp229_lsf/ttp229_lsf.h | 1 - esphome/components/vbus/vbus.h | 1 - esphome/components/veml3235/veml3235.h | 1 - esphome/components/veml7700/veml7700.h | 1 - esphome/components/vl53l0x/vl53l0x_sensor.h | 1 - esphome/components/xiaomi_cgd1/xiaomi_cgd1.h | 1 - esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h | 1 - esphome/components/xiaomi_cgg1/xiaomi_cgg1.h | 1 - esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h | 1 - esphome/components/xiaomi_gcls002/xiaomi_gcls002.h | 1 - esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h | 1 - esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h | 1 - esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h | 1 - esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h | 1 - esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h | 1 - esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h | 1 - esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h | 1 - esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h | 1 - esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h | 1 - esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h | 1 - esphome/components/xiaomi_miscale/xiaomi_miscale.h | 1 - esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h | 1 - esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h | 1 - esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h | 1 - esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h | 1 - esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h | 1 - esphome/components/zio_ultrasonic/zio_ultrasonic.h | 2 -- esphome/components/zyaura/zyaura.h | 1 - 129 files changed, 147 deletions(-) diff --git a/esphome/components/ade7880/ade7880.h b/esphome/components/ade7880/ade7880.h index a565357dc5..40bc22e54a 100644 --- a/esphome/components/ade7880/ade7880.h +++ b/esphome/components/ade7880/ade7880.h @@ -85,8 +85,6 @@ class ADE7880 : public i2c::I2CDevice, public PollingComponent { void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: ADE7880Store store_{}; InternalGPIOPin *irq0_pin_{nullptr}; diff --git a/esphome/components/ads1115/ads1115.h b/esphome/components/ads1115/ads1115.h index e65835a386..e827a739d2 100644 --- a/esphome/components/ads1115/ads1115.h +++ b/esphome/components/ads1115/ads1115.h @@ -49,7 +49,6 @@ class ADS1115Component : public Component, public i2c::I2CDevice { void setup() override; void dump_config() override; /// HARDWARE_LATE setup priority - float get_setup_priority() const override { return setup_priority::DATA; } void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; } /// Helper method to request a measurement from a sensor. diff --git a/esphome/components/ads1118/ads1118.h b/esphome/components/ads1118/ads1118.h index 8b9aa15cd2..e96baab386 100644 --- a/esphome/components/ads1118/ads1118.h +++ b/esphome/components/ads1118/ads1118.h @@ -34,7 +34,6 @@ class ADS1118 : public Component, ADS1118() = default; void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } /// Helper method to request a measurement from a sensor. float request_measurement(ADS1118Multiplexer multiplexer, ADS1118Gain gain, bool temperature_mode); diff --git a/esphome/components/ags10/ags10.h b/esphome/components/ags10/ags10.h index f2201fe70c..3e184ae176 100644 --- a/esphome/components/ags10/ags10.h +++ b/esphome/components/ags10/ags10.h @@ -31,8 +31,6 @@ class AGS10Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - /** * Modifies target address of AGS10. * diff --git a/esphome/components/aic3204/aic3204.h b/esphome/components/aic3204/aic3204.h index 783a58a2b9..28006e33fc 100644 --- a/esphome/components/aic3204/aic3204.h +++ b/esphome/components/aic3204/aic3204.h @@ -66,7 +66,6 @@ class AIC3204 : public audio_dac::AudioDac, public Component, public i2c::I2CDev public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } bool set_mute_off() override; bool set_mute_on() override; diff --git a/esphome/components/alpha3/alpha3.h b/esphome/components/alpha3/alpha3.h index 325c70a538..7189ecbc33 100644 --- a/esphome/components/alpha3/alpha3.h +++ b/esphome/components/alpha3/alpha3.h @@ -41,7 +41,6 @@ class Alpha3 : public esphome::ble_client::BLEClientNode, public PollingComponen void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; } void set_head_sensor(sensor::Sensor *sensor) { this->head_sensor_ = sensor; } void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; } diff --git a/esphome/components/am43/cover/am43_cover.h b/esphome/components/am43/cover/am43_cover.h index f33f2d1734..d6d020e98c 100644 --- a/esphome/components/am43/cover/am43_cover.h +++ b/esphome/components/am43/cover/am43_cover.h @@ -22,7 +22,6 @@ class Am43Component : public cover::Cover, public esphome::ble_client::BLEClient void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } cover::CoverTraits get_traits() override; void set_pin(uint16_t pin) { this->pin_ = pin; } void set_invert_position(bool invert_position) { this->invert_position_ = invert_position; } diff --git a/esphome/components/am43/sensor/am43_sensor.h b/esphome/components/am43/sensor/am43_sensor.h index 8dfe83e3a3..91973d8e33 100644 --- a/esphome/components/am43/sensor/am43_sensor.h +++ b/esphome/components/am43/sensor/am43_sensor.h @@ -22,7 +22,6 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_battery(sensor::Sensor *battery) { battery_ = battery; } void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h index efb8e3c90c..55d6b15c36 100644 --- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h @@ -12,8 +12,6 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina void dump_config() override; void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void set_sensor(sensor::Sensor *analog_sensor); template void set_upper_threshold(T upper_threshold) { this->upper_threshold_ = upper_threshold; } template void set_lower_threshold(T lower_threshold) { this->lower_threshold_ = lower_threshold; } diff --git a/esphome/components/anova/anova.h b/esphome/components/anova/anova.h index 3d1394980a..560d96baa7 100644 --- a/esphome/components/anova/anova.h +++ b/esphome/components/anova/anova.h @@ -26,7 +26,6 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } climate::ClimateTraits traits() override { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); diff --git a/esphome/components/as5600/as5600.h b/esphome/components/as5600/as5600.h index fbfd18db40..914a4431bd 100644 --- a/esphome/components/as5600/as5600.h +++ b/esphome/components/as5600/as5600.h @@ -50,7 +50,6 @@ class AS5600Component : public Component, public i2c::I2CDevice { void setup() override; void dump_config() override; /// HARDWARE_LATE setup priority - float get_setup_priority() const override { return setup_priority::DATA; } // configuration setters void set_dir_pin(InternalGPIOPin *pin) { this->dir_pin_ = pin; } diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.h b/esphome/components/atc_mithermometer/atc_mithermometer.h index 31fb77ac7f..d22e3f069b 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.h +++ b/esphome/components/atc_mithermometer/atc_mithermometer.h @@ -25,7 +25,6 @@ class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevice bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/b_parasite/b_parasite.h b/esphome/components/b_parasite/b_parasite.h index 70ee4ab23c..7dd08968ec 100644 --- a/esphome/components/b_parasite/b_parasite.h +++ b/esphome/components/b_parasite/b_parasite.h @@ -16,7 +16,6 @@ class BParasite : public Component, public esp32_ble_tracker::ESPBTDeviceListene bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_battery_voltage(sensor::Sensor *battery_voltage) { battery_voltage_ = battery_voltage; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } diff --git a/esphome/components/ble_client/output/ble_binary_output.h b/esphome/components/ble_client/output/ble_binary_output.h index 0a1e186b26..5e8bd6da62 100644 --- a/esphome/components/ble_client/output/ble_binary_output.h +++ b/esphome/components/ble_client/output/ble_binary_output.h @@ -16,7 +16,6 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi public: void dump_config() override; void loop() override {} - float get_setup_priority() const override { return setup_priority::DATA; } void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.h b/esphome/components/ble_client/sensor/ble_rssi_sensor.h index 5dd3fc7af9..76cd8345a6 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.h +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.h @@ -18,7 +18,6 @@ class BLEClientRSSISensor : public sensor::Sensor, public PollingComponent, publ void loop() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; diff --git a/esphome/components/ble_client/sensor/ble_sensor.h b/esphome/components/ble_client/sensor/ble_sensor.h index b11a010ee4..24d1ed2fd2 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.h +++ b/esphome/components/ble_client/sensor/ble_sensor.h @@ -24,7 +24,6 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } diff --git a/esphome/components/ble_client/switch/ble_switch.h b/esphome/components/ble_client/switch/ble_switch.h index 2e19c8aeef..9809f904e7 100644 --- a/esphome/components/ble_client/switch/ble_switch.h +++ b/esphome/components/ble_client/switch/ble_switch.h @@ -19,7 +19,6 @@ class BLEClientSwitch : public switch_::Switch, public Component, public BLEClie void loop() override {} void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void write_state(bool state) override; diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.h b/esphome/components/ble_client/text_sensor/ble_text_sensor.h index cb34043b46..c75a4df952 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.h +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.h @@ -20,7 +20,6 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index 3ed60d1b49..70ecc67c32 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -105,7 +105,6 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, this->set_found_(false); } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void set_found_(bool state) { diff --git a/esphome/components/ble_rssi/ble_rssi_sensor.h b/esphome/components/ble_rssi/ble_rssi_sensor.h index 89e4f33aca..80245a1fe1 100644 --- a/esphome/components/ble_rssi/ble_rssi_sensor.h +++ b/esphome/components/ble_rssi/ble_rssi_sensor.h @@ -99,7 +99,6 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi return false; } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: enum MatchType { MATCH_BY_MAC_ADDRESS, MATCH_BY_IRK, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID }; diff --git a/esphome/components/ble_scanner/ble_scanner.h b/esphome/components/ble_scanner/ble_scanner.h index b330eff696..8bb51fcff2 100644 --- a/esphome/components/ble_scanner/ble_scanner.h +++ b/esphome/components/ble_scanner/ble_scanner.h @@ -29,7 +29,6 @@ class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESP return true; } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } }; } // namespace ble_scanner diff --git a/esphome/components/bmp581/bmp581.h b/esphome/components/bmp581/bmp581.h index 7327be44ae..1d7e932fa1 100644 --- a/esphome/components/bmp581/bmp581.h +++ b/esphome/components/bmp581/bmp581.h @@ -61,8 +61,6 @@ enum IIRFilter { class BMP581Component : public PollingComponent, public i2c::I2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } - void dump_config() override; void setup() override; diff --git a/esphome/components/cap1188/cap1188.h b/esphome/components/cap1188/cap1188.h index fa0ed622fa..baefd1c48f 100644 --- a/esphome/components/cap1188/cap1188.h +++ b/esphome/components/cap1188/cap1188.h @@ -46,7 +46,6 @@ class CAP1188Component : public Component, public i2c::I2CDevice { void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; protected: diff --git a/esphome/components/ccs811/ccs811.h b/esphome/components/ccs811/ccs811.h index 8a0d60d002..675ba7da97 100644 --- a/esphome/components/ccs811/ccs811.h +++ b/esphome/components/ccs811/ccs811.h @@ -25,8 +25,6 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: optional read_status_() { return this->read_byte(0x00); } bool status_has_error_() { return this->read_status_().value_or(1) & 1; } diff --git a/esphome/components/copy/binary_sensor/copy_binary_sensor.h b/esphome/components/copy/binary_sensor/copy_binary_sensor.h index d62ed13c76..fc1e368b38 100644 --- a/esphome/components/copy/binary_sensor/copy_binary_sensor.h +++ b/esphome/components/copy/binary_sensor/copy_binary_sensor.h @@ -11,7 +11,6 @@ class CopyBinarySensor : public binary_sensor::BinarySensor, public Component { void set_source(binary_sensor::BinarySensor *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: binary_sensor::BinarySensor *source_; diff --git a/esphome/components/copy/button/copy_button.h b/esphome/components/copy/button/copy_button.h index 9996ca0c65..79d5dbcf04 100644 --- a/esphome/components/copy/button/copy_button.h +++ b/esphome/components/copy/button/copy_button.h @@ -10,7 +10,6 @@ class CopyButton : public button::Button, public Component { public: void set_source(button::Button *source) { source_ = source; } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void press_action() override; diff --git a/esphome/components/copy/cover/copy_cover.h b/esphome/components/copy/cover/copy_cover.h index fb278523ff..ec27b6782a 100644 --- a/esphome/components/copy/cover/copy_cover.h +++ b/esphome/components/copy/cover/copy_cover.h @@ -11,7 +11,6 @@ class CopyCover : public cover::Cover, public Component { void set_source(cover::Cover *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } cover::CoverTraits get_traits() override; diff --git a/esphome/components/copy/fan/copy_fan.h b/esphome/components/copy/fan/copy_fan.h index 1a69810510..b474975bc4 100644 --- a/esphome/components/copy/fan/copy_fan.h +++ b/esphome/components/copy/fan/copy_fan.h @@ -11,7 +11,6 @@ class CopyFan : public fan::Fan, public Component { void set_source(fan::Fan *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } fan::FanTraits get_traits() override; diff --git a/esphome/components/copy/lock/copy_lock.h b/esphome/components/copy/lock/copy_lock.h index 0554013674..8799eebb4a 100644 --- a/esphome/components/copy/lock/copy_lock.h +++ b/esphome/components/copy/lock/copy_lock.h @@ -11,7 +11,6 @@ class CopyLock : public lock::Lock, public Component { void set_source(lock::Lock *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void control(const lock::LockCall &call) override; diff --git a/esphome/components/copy/number/copy_number.h b/esphome/components/copy/number/copy_number.h index 1ad956fec4..09b65e2cbf 100644 --- a/esphome/components/copy/number/copy_number.h +++ b/esphome/components/copy/number/copy_number.h @@ -11,7 +11,6 @@ class CopyNumber : public number::Number, public Component { void set_source(number::Number *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void control(float value) override; diff --git a/esphome/components/copy/select/copy_select.h b/esphome/components/copy/select/copy_select.h index c8666cd394..fb0aee86f6 100644 --- a/esphome/components/copy/select/copy_select.h +++ b/esphome/components/copy/select/copy_select.h @@ -11,7 +11,6 @@ class CopySelect : public select::Select, public Component { void set_source(select::Select *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void control(const std::string &value) override; diff --git a/esphome/components/copy/sensor/copy_sensor.h b/esphome/components/copy/sensor/copy_sensor.h index 1ae790ada3..500e6872fe 100644 --- a/esphome/components/copy/sensor/copy_sensor.h +++ b/esphome/components/copy/sensor/copy_sensor.h @@ -11,7 +11,6 @@ class CopySensor : public sensor::Sensor, public Component { void set_source(sensor::Sensor *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: sensor::Sensor *source_; diff --git a/esphome/components/copy/switch/copy_switch.h b/esphome/components/copy/switch/copy_switch.h index 26cb254ab3..80310af03f 100644 --- a/esphome/components/copy/switch/copy_switch.h +++ b/esphome/components/copy/switch/copy_switch.h @@ -11,7 +11,6 @@ class CopySwitch : public switch_::Switch, public Component { void set_source(switch_::Switch *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void write_state(bool state) override; diff --git a/esphome/components/copy/text/copy_text.h b/esphome/components/copy/text/copy_text.h index beb8610dfe..9eaebae4be 100644 --- a/esphome/components/copy/text/copy_text.h +++ b/esphome/components/copy/text/copy_text.h @@ -11,7 +11,6 @@ class CopyText : public text::Text, public Component { void set_source(text::Text *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void control(const std::string &value) override; diff --git a/esphome/components/copy/text_sensor/copy_text_sensor.h b/esphome/components/copy/text_sensor/copy_text_sensor.h index fe91fe948b..489986c59d 100644 --- a/esphome/components/copy/text_sensor/copy_text_sensor.h +++ b/esphome/components/copy/text_sensor/copy_text_sensor.h @@ -11,7 +11,6 @@ class CopyTextSensor : public text_sensor::TextSensor, public Component { void set_source(text_sensor::TextSensor *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: text_sensor::TextSensor *source_; diff --git a/esphome/components/cs5460a/cs5460a.h b/esphome/components/cs5460a/cs5460a.h index 763ddc14fa..15ae04f3c6 100644 --- a/esphome/components/cs5460a/cs5460a.h +++ b/esphome/components/cs5460a/cs5460a.h @@ -77,7 +77,6 @@ class CS5460AComponent : public Component, void setup() override; void loop() override {} - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; protected: diff --git a/esphome/components/duty_time/duty_time_sensor.h b/esphome/components/duty_time/duty_time_sensor.h index 38655f104a..18280f8e21 100644 --- a/esphome/components/duty_time/duty_time_sensor.h +++ b/esphome/components/duty_time/duty_time_sensor.h @@ -19,7 +19,6 @@ class DutyTimeSensor : public sensor::Sensor, public PollingComponent { void update() override; void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void start(); void stop(); diff --git a/esphome/components/ens160_base/ens160_base.h b/esphome/components/ens160_base/ens160_base.h index 729225a5ae..ae850c8180 100644 --- a/esphome/components/ens160_base/ens160_base.h +++ b/esphome/components/ens160_base/ens160_base.h @@ -18,7 +18,6 @@ class ENS160Component : public PollingComponent, public sensor::Sensor { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void send_env_data_(); diff --git a/esphome/components/es7210/es7210.h b/esphome/components/es7210/es7210.h index 8f6d9d8136..7071a547ec 100644 --- a/esphome/components/es7210/es7210.h +++ b/esphome/components/es7210/es7210.h @@ -25,7 +25,6 @@ class ES7210 : public audio_adc::AudioAdc, public Component, public i2c::I2CDevi */ public: void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; void set_bits_per_sample(ES7210BitsPerSample bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } diff --git a/esphome/components/es7243e/es7243e.h b/esphome/components/es7243e/es7243e.h index 41a8acac8d..f7c9d67371 100644 --- a/esphome/components/es7243e/es7243e.h +++ b/esphome/components/es7243e/es7243e.h @@ -14,7 +14,6 @@ class ES7243E : public audio_adc::AudioAdc, public Component, public i2c::I2CDev */ public: void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; bool set_mic_gain(float mic_gain) override; diff --git a/esphome/components/es8156/es8156.h b/esphome/components/es8156/es8156.h index e973599a7a..082514485c 100644 --- a/esphome/components/es8156/es8156.h +++ b/esphome/components/es8156/es8156.h @@ -14,7 +14,6 @@ class ES8156 : public audio_dac::AudioDac, public Component, public i2c::I2CDevi ///////////////////////// void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; //////////////////////// diff --git a/esphome/components/es8311/es8311.h b/esphome/components/es8311/es8311.h index 840a07204c..5eccc48004 100644 --- a/esphome/components/es8311/es8311.h +++ b/esphome/components/es8311/es8311.h @@ -50,7 +50,6 @@ class ES8311 : public audio_dac::AudioDac, public Component, public i2c::I2CDevi ///////////////////////// void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; //////////////////////// diff --git a/esphome/components/es8388/es8388.h b/esphome/components/es8388/es8388.h index 45944f68bd..373f71b437 100644 --- a/esphome/components/es8388/es8388.h +++ b/esphome/components/es8388/es8388.h @@ -38,7 +38,6 @@ class ES8388 : public audio_dac::AudioDac, public Component, public i2c::I2CDevi ///////////////////////// void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; //////////////////////// diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 0eac590ce7..3fce8a7e18 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -52,7 +52,6 @@ class ESP32TouchComponent : public Component { void setup() override; void dump_config() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } void on_shutdown() override; diff --git a/esphome/components/ezo/ezo.h b/esphome/components/ezo/ezo.h index 28b46643e9..00dd98fc80 100644 --- a/esphome/components/ezo/ezo.h +++ b/esphome/components/ezo/ezo.h @@ -38,7 +38,6 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2 void loop() override; void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; }; // I2C void set_address(uint8_t address); diff --git a/esphome/components/ezo_pmp/ezo_pmp.h b/esphome/components/ezo_pmp/ezo_pmp.h index b41710cd78..671e124810 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.h +++ b/esphome/components/ezo_pmp/ezo_pmp.h @@ -23,7 +23,6 @@ namespace ezo_pmp { class EzoPMP : public PollingComponent, public i2c::I2CDevice { public: void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void loop() override; void update() override; diff --git a/esphome/components/feedback/feedback_cover.h b/esphome/components/feedback/feedback_cover.h index 7e107aebcd..199d3b520a 100644 --- a/esphome/components/feedback/feedback_cover.h +++ b/esphome/components/feedback/feedback_cover.h @@ -16,7 +16,6 @@ class FeedbackCover : public cover::Cover, public Component { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; Trigger<> *get_open_trigger() const { return this->open_trigger_; } Trigger<> *get_close_trigger() const { return this->close_trigger_; } diff --git a/esphome/components/fs3000/fs3000.h b/esphome/components/fs3000/fs3000.h index be3680e7e1..e33c72215f 100644 --- a/esphome/components/fs3000/fs3000.h +++ b/esphome/components/fs3000/fs3000.h @@ -18,7 +18,6 @@ class FS3000Component : public PollingComponent, public i2c::I2CDevice, public s void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_model(FS3000Model model) { this->model_ = model; } diff --git a/esphome/components/gcja5/gcja5.h b/esphome/components/gcja5/gcja5.h index ea1fb78bf0..30bc877169 100644 --- a/esphome/components/gcja5/gcja5.h +++ b/esphome/components/gcja5/gcja5.h @@ -12,7 +12,6 @@ class GCJA5Component : public Component, public uart::UARTDevice { public: void dump_config() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; } void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; } diff --git a/esphome/components/gp8403/gp8403.h b/esphome/components/gp8403/gp8403.h index 65182ef301..9f493d39e3 100644 --- a/esphome/components/gp8403/gp8403.h +++ b/esphome/components/gp8403/gp8403.h @@ -15,7 +15,6 @@ class GP8403 : public Component, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_voltage(gp8403::GP8403Voltage voltage) { this->voltage_ = voltage; } diff --git a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h index 1987d33f37..aab881bd05 100644 --- a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h +++ b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h @@ -22,8 +22,6 @@ class GroveGasMultichannelV2Component : public PollingComponent, public i2c::I2C void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: enum ErrorCode { UNKNOWN, diff --git a/esphome/components/he60r/he60r.h b/esphome/components/he60r/he60r.h index e41e2203c1..02a2b44e66 100644 --- a/esphome/components/he60r/he60r.h +++ b/esphome/components/he60r/he60r.h @@ -13,7 +13,6 @@ class HE60rCover : public cover::Cover, public Component, public uart::UARTDevic void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void set_open_duration(uint32_t duration) { this->open_duration_ = duration; } void set_close_duration(uint32_t duration) { this->close_duration_ = duration; } diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.h b/esphome/components/honeywellabp2_i2c/honeywellabp2.h index bc81524ac2..274de847ac 100644 --- a/esphome/components/honeywellabp2_i2c/honeywellabp2.h +++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.h @@ -18,7 +18,6 @@ class HONEYWELLABP2Sensor : public PollingComponent, public i2c::I2CDevice { void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }; void loop() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void dump_config() override; void read_sensor_data(); diff --git a/esphome/components/i2c_device/i2c_device.h b/esphome/components/i2c_device/i2c_device.h index ab118e3e89..9944ca9204 100644 --- a/esphome/components/i2c_device/i2c_device.h +++ b/esphome/components/i2c_device/i2c_device.h @@ -9,7 +9,6 @@ namespace i2c_device { class I2CDeviceComponent : public Component, public i2c::I2CDevice { public: void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: }; diff --git a/esphome/components/iaqcore/iaqcore.h b/esphome/components/iaqcore/iaqcore.h index f343c2a705..bb0bfcc754 100644 --- a/esphome/components/iaqcore/iaqcore.h +++ b/esphome/components/iaqcore/iaqcore.h @@ -16,8 +16,6 @@ class IAQCore : public PollingComponent, public i2c::I2CDevice { void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: sensor::Sensor *co2_{nullptr}; sensor::Sensor *tvoc_{nullptr}; diff --git a/esphome/components/ina260/ina260.h b/esphome/components/ina260/ina260.h index 8bad1cba6d..6cbc157cf3 100644 --- a/esphome/components/ina260/ina260.h +++ b/esphome/components/ina260/ina260.h @@ -13,8 +13,6 @@ class INA260Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void set_bus_voltage_sensor(sensor::Sensor *bus_voltage_sensor) { this->bus_voltage_sensor_ = bus_voltage_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; } diff --git a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h index bdca2d0cac..cd2ea99717 100644 --- a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h +++ b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h @@ -16,7 +16,6 @@ class InkbirdIbstH1Mini : public Component, public esp32_ble_tracker::ESPBTDevic bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_external_temperature(sensor::Sensor *external_temperature) { external_temperature_ = external_temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index e84d7a8ed1..d9f2f5e50f 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -27,7 +27,6 @@ class IntegrationSensor : public sensor::Sensor, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_sensor(Sensor *sensor) { sensor_ = sensor; } void set_time(IntegrationSensorTime time) { time_ = time; } void set_method(IntegrationMethod method) { method_ = method; } diff --git a/esphome/components/interval/interval.h b/esphome/components/interval/interval.h index 5b8bc3081f..8f904b104d 100644 --- a/esphome/components/interval/interval.h +++ b/esphome/components/interval/interval.h @@ -23,8 +23,6 @@ class IntervalTrigger : public Trigger<>, public PollingComponent { void set_startup_delay(const uint32_t startup_delay) { this->startup_delay_ = startup_delay; } - float get_setup_priority() const override { return setup_priority::DATA; } - protected: uint32_t startup_delay_{0}; bool started_{false}; diff --git a/esphome/components/ltr390/ltr390.h b/esphome/components/ltr390/ltr390.h index 7359cbd336..7db73d68ff 100644 --- a/esphome/components/ltr390/ltr390.h +++ b/esphome/components/ltr390/ltr390.h @@ -44,7 +44,6 @@ enum LTR390RESOLUTION { class LTR390Component : public PollingComponent, public i2c::I2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h index 07b69fa0d0..849ff6bc23 100644 --- a/esphome/components/ltr501/ltr501.h +++ b/esphome/components/ltr501/ltr501.h @@ -25,7 +25,6 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { // // EspHome framework functions // - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.h b/esphome/components/ltr_als_ps/ltr_als_ps.h index 4cbbcea54c..2c768009ab 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.h +++ b/esphome/components/ltr_als_ps/ltr_als_ps.h @@ -25,7 +25,6 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { // // EspHome framework functions // - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/max9611/max9611.h b/esphome/components/max9611/max9611.h index 017f56b1a7..1eb7542aee 100644 --- a/esphome/components/max9611/max9611.h +++ b/esphome/components/max9611/max9611.h @@ -38,7 +38,6 @@ class MAX9611Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void update() override; void set_voltage_sensor(sensor::Sensor *vs) { voltage_sensor_ = vs; } void set_current_sensor(sensor::Sensor *cs) { current_sensor_ = cs; } diff --git a/esphome/components/mcp9600/mcp9600.h b/esphome/components/mcp9600/mcp9600.h index 92612cc26d..c414653ea6 100644 --- a/esphome/components/mcp9600/mcp9600.h +++ b/esphome/components/mcp9600/mcp9600.h @@ -24,8 +24,6 @@ class MCP9600Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void set_hot_junction(sensor::Sensor *hot_junction) { this->hot_junction_sensor_ = hot_junction; } void set_cold_junction(sensor::Sensor *cold_junction) { this->cold_junction_sensor_ = cold_junction; } void set_thermocouple_type(MCP9600ThermocoupleType thermocouple_type) { diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.h b/esphome/components/mopeka_pro_check/mopeka_pro_check.h index c58406ac18..4cbe8f2afe 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.h +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.h @@ -34,7 +34,6 @@ class MopekaProCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_min_signal_quality(SensorReadQuality min) { this->min_signal_quality_ = min; }; void set_level(sensor::Sensor *level) { level_ = level; }; diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h index 2a1d9d2dfc..b92445df34 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.h +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -48,7 +48,6 @@ class MopekaStdCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_level(sensor::Sensor *level) { this->level_ = level; }; void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }; diff --git a/esphome/components/mpl3115a2/mpl3115a2.h b/esphome/components/mpl3115a2/mpl3115a2.h index 00a6d90c52..05da71f830 100644 --- a/esphome/components/mpl3115a2/mpl3115a2.h +++ b/esphome/components/mpl3115a2/mpl3115a2.h @@ -91,8 +91,6 @@ class MPL3115A2Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: sensor::Sensor *temperature_{nullptr}; sensor::Sensor *altitude_{nullptr}; diff --git a/esphome/components/ms8607/ms8607.h b/esphome/components/ms8607/ms8607.h index 0bee7e97b7..67ce2817fa 100644 --- a/esphome/components/ms8607/ms8607.h +++ b/esphome/components/ms8607/ms8607.h @@ -37,7 +37,6 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } diff --git a/esphome/components/pmsa003i/pmsa003i.h b/esphome/components/pmsa003i/pmsa003i.h index 59f39a7314..cd106704a6 100644 --- a/esphome/components/pmsa003i/pmsa003i.h +++ b/esphome/components/pmsa003i/pmsa003i.h @@ -32,7 +32,6 @@ class PMSA003IComponent : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_standard_units(bool standard_units) { this->standard_units_ = standard_units; } diff --git a/esphome/components/pmsx003/pmsx003.h b/esphome/components/pmsx003/pmsx003.h index e422d4165b..ba607b4487 100644 --- a/esphome/components/pmsx003/pmsx003.h +++ b/esphome/components/pmsx003/pmsx003.h @@ -31,7 +31,6 @@ enum PMSX003State { class PMSX003Component : public uart::UARTDevice, public Component { public: PMSX003Component() = default; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; void loop() override; diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h index 87af7d629b..42cd7a6ef7 100644 --- a/esphome/components/pn7150/pn7150.h +++ b/esphome/components/pn7150/pn7150.h @@ -146,7 +146,6 @@ class PN7150 : public nfc::Nfcc, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; void set_irq_pin(GPIOPin *irq_pin) { this->irq_pin_ = irq_pin; } diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h index ff8a492b7b..fc00296a71 100644 --- a/esphome/components/pn7160/pn7160.h +++ b/esphome/components/pn7160/pn7160.h @@ -161,7 +161,6 @@ class PN7160 : public nfc::Nfcc, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; void set_dwl_req_pin(GPIOPin *dwl_req_pin) { this->dwl_req_pin_ = dwl_req_pin; } diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index cea9fa7bf9..5ba59cca2a 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -76,7 +76,6 @@ class PulseCounterSensor : public sensor::Sensor, public PollingComponent { /// Unit of measurement is "pulses/min". void setup() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; protected: diff --git a/esphome/components/pulse_width/pulse_width.h b/esphome/components/pulse_width/pulse_width.h index 822688ec88..c6b896988d 100644 --- a/esphome/components/pulse_width/pulse_width.h +++ b/esphome/components/pulse_width/pulse_width.h @@ -32,7 +32,6 @@ class PulseWidthSensor : public sensor::Sensor, public PollingComponent { void set_pin(InternalGPIOPin *pin) { pin_ = pin; } void setup() override { this->store_.setup(this->pin_); } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void update() override; protected: diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.h b/esphome/components/pvvx_mithermometer/display/pvvx_display.h index dfeb49c49d..9739362024 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.h +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.h @@ -39,8 +39,6 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void update() override; void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h index 99455a1663..9614a3c586 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h @@ -25,7 +25,6 @@ class PVVXMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevic bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/qwiic_pir/qwiic_pir.h b/esphome/components/qwiic_pir/qwiic_pir.h index d58d67734f..797ded2cc6 100644 --- a/esphome/components/qwiic_pir/qwiic_pir.h +++ b/esphome/components/qwiic_pir/qwiic_pir.h @@ -36,7 +36,6 @@ class QwiicPIRComponent : public Component, public i2c::I2CDevice, public binary void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_debounce_time(uint16_t debounce_time) { this->debounce_time_ = debounce_time; } void set_debounce_mode(DebounceMode mode) { this->debounce_mode_ = mode; } diff --git a/esphome/components/rc522/rc522.h b/esphome/components/rc522/rc522.h index c6c5e119f0..437cea808b 100644 --- a/esphome/components/rc522/rc522.h +++ b/esphome/components/rc522/rc522.h @@ -19,7 +19,6 @@ class RC522 : public PollingComponent { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void loop() override; diff --git a/esphome/components/rdm6300/rdm6300.h b/esphome/components/rdm6300/rdm6300.h index 1a1a0c0cd6..24a808b62c 100644 --- a/esphome/components/rdm6300/rdm6300.h +++ b/esphome/components/rdm6300/rdm6300.h @@ -21,8 +21,6 @@ class RDM6300Component : public Component, public uart::UARTDevice { void register_card(RDM6300BinarySensor *obj) { this->cards_.push_back(obj); } void register_trigger(RDM6300Trigger *trig) { this->triggers_.push_back(trig); } - float get_setup_priority() const override { return setup_priority::DATA; } - protected: int8_t read_state_{-1}; uint8_t buffer_[6]{}; diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 9d844eee66..45e06e664a 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -59,7 +59,6 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void setup() override; void dump_config() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } #ifdef USE_ESP32 void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; } diff --git a/esphome/components/resistance/resistance_sensor.h b/esphome/components/resistance/resistance_sensor.h index b57f90b59c..a3b6e92c59 100644 --- a/esphome/components/resistance/resistance_sensor.h +++ b/esphome/components/resistance/resistance_sensor.h @@ -24,7 +24,6 @@ class ResistanceSensor : public Component, public sensor::Sensor { this->process_(this->sensor_->state); } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void process_(float value); diff --git a/esphome/components/ruuvitag/ruuvitag.h b/esphome/components/ruuvitag/ruuvitag.h index 63029ebb4d..dfe393724c 100644 --- a/esphome/components/ruuvitag/ruuvitag.h +++ b/esphome/components/ruuvitag/ruuvitag.h @@ -48,7 +48,6 @@ class RuuviTag : public Component, public esp32_ble_tracker::ESPBTDeviceListener } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_pressure(sensor::Sensor *pressure) { pressure_ = pressure; } diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h index 40f075e673..ed3f5e7e9a 100644 --- a/esphome/components/scd30/scd30.h +++ b/esphome/components/scd30/scd30.h @@ -26,7 +26,6 @@ class SCD30Component : public Component, public sensirion_common::SensirionI2CDe void setup() override; void update(); void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: bool is_data_ready_(); diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h index 22055e78d0..f2efb28ac1 100644 --- a/esphome/components/scd4x/scd4x.h +++ b/esphome/components/scd4x/scd4x.h @@ -19,7 +19,6 @@ enum MeasurementMode { PERIODIC, LOW_POWER_PERIODIC, SINGLE_SHOT, SINGLE_SHOT_RH class SCD4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 165f90ed11..60175ec933 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -239,8 +239,6 @@ template class ScriptWaitAction : public Action, this->play_next_tuple_(this->var_); } - float get_setup_priority() const override { return setup_priority::DATA; } - void play(Ts... x) override { /* ignore - see play_complex */ } diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index 6d90636a89..0fa31605e6 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -48,7 +48,6 @@ struct TemperatureCompensation { class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/senseair/senseair.h b/esphome/components/senseair/senseair.h index bcec638f79..9f939d5b07 100644 --- a/esphome/components/senseair/senseair.h +++ b/esphome/components/senseair/senseair.h @@ -10,7 +10,6 @@ namespace senseair { class SenseAirComponent : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } void update() override; diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index 92d18bf601..ff1708dc53 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -20,7 +20,6 @@ class Servo : public Component { void detach(); void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_min_level(float min_level) { min_level_ = min_level; } void set_idle_level(float idle_level) { idle_level_ = idle_level; } void set_max_level(float max_level) { max_level_ = max_level; } diff --git a/esphome/components/sfa30/sfa30.h b/esphome/components/sfa30/sfa30.h index fa2c59f624..2b744b8da4 100644 --- a/esphome/components/sfa30/sfa30.h +++ b/esphome/components/sfa30/sfa30.h @@ -11,7 +11,6 @@ class SFA30Component : public PollingComponent, public sensirion_common::Sensiri enum ErrorCode { DEVICE_MARKING_READ_FAILED, MEASUREMENT_INIT_FAILED, UNKNOWN }; public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h index 9e882e6b05..e6429a7bfa 100644 --- a/esphome/components/sgp30/sgp30.h +++ b/esphome/components/sgp30/sgp30.h @@ -32,7 +32,6 @@ class SGP30Component : public PollingComponent, public sensirion_common::Sensiri void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void send_env_data_(); diff --git a/esphome/components/sgp4x/sgp4x.h b/esphome/components/sgp4x/sgp4x.h index 45ee66af68..8b31bca28c 100644 --- a/esphome/components/sgp4x/sgp4x.h +++ b/esphome/components/sgp4x/sgp4x.h @@ -75,7 +75,6 @@ class SGP4xComponent : public PollingComponent, public sensor::Sensor, public se void update() override; void take_sample(); void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; } void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; } void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; } diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h index 98e0629b50..accc7323be 100644 --- a/esphome/components/sht4x/sht4x.h +++ b/esphome/components/sht4x/sht4x.h @@ -17,7 +17,6 @@ enum SHT4XHEATERTIME : uint16_t { SHT4X_HEATERTIME_LONG = 1100, SHT4X_HEATERTIME class SHT4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/sm300d2/sm300d2.h b/esphome/components/sm300d2/sm300d2.h index 88c04e9813..4e97b54988 100644 --- a/esphome/components/sm300d2/sm300d2.h +++ b/esphome/components/sm300d2/sm300d2.h @@ -9,8 +9,6 @@ namespace sm300d2 { class SM300D2Sensor : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } - void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } void set_formaldehyde_sensor(sensor::Sensor *formaldehyde_sensor) { formaldehyde_sensor_ = formaldehyde_sensor; } void set_tvoc_sensor(sensor::Sensor *tvoc_sensor) { tvoc_sensor_ = tvoc_sensor; } diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h index cf2e7a7d4f..04189247e8 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -26,7 +26,6 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } bool start_fan_cleaning(); diff --git a/esphome/components/status/status_binary_sensor.h b/esphome/components/status/status_binary_sensor.h index 08aa0fb32f..feda8b6328 100644 --- a/esphome/components/status/status_binary_sensor.h +++ b/esphome/components/status/status_binary_sensor.h @@ -13,8 +13,6 @@ class StatusBinarySensor : public binary_sensor::BinarySensor, public Component void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - bool is_status_binary_sensor() const override { return true; } }; diff --git a/esphome/components/switch/binary_sensor/switch_binary_sensor.h b/esphome/components/switch/binary_sensor/switch_binary_sensor.h index 5a947c2fb4..53b07da903 100644 --- a/esphome/components/switch/binary_sensor/switch_binary_sensor.h +++ b/esphome/components/switch/binary_sensor/switch_binary_sensor.h @@ -12,7 +12,6 @@ class SwitchBinarySensor : public binary_sensor::BinarySensor, public Component void set_source(Switch *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: Switch *source_; diff --git a/esphome/components/tmp1075/tmp1075.h b/esphome/components/tmp1075/tmp1075.h index 84e2e8abe4..b5fd60c08e 100644 --- a/esphome/components/tmp1075/tmp1075.h +++ b/esphome/components/tmp1075/tmp1075.h @@ -58,8 +58,6 @@ class TMP1075Sensor : public PollingComponent, public sensor::Sensor, public i2c void setup() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void dump_config() override; // Call write_config() after calling any of these to send the new config to diff --git a/esphome/components/tof10120/tof10120_sensor.h b/esphome/components/tof10120/tof10120_sensor.h index 90bad8ed07..d0cca19d4c 100644 --- a/esphome/components/tof10120/tof10120_sensor.h +++ b/esphome/components/tof10120/tof10120_sensor.h @@ -12,7 +12,6 @@ class TOF10120Sensor : public sensor::Sensor, public PollingComponent, public i2 void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void update() override; }; } // namespace tof10120 diff --git a/esphome/components/tormatic/tormatic_cover.h b/esphome/components/tormatic/tormatic_cover.h index 33a2e1db8f..534d4bef14 100644 --- a/esphome/components/tormatic/tormatic_cover.h +++ b/esphome/components/tormatic/tormatic_cover.h @@ -16,7 +16,6 @@ class Tormatic : public cover::Cover, public uart::UARTDevice, public PollingCom void loop() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void set_open_duration(uint32_t duration) { this->open_duration_ = duration; } void set_close_duration(uint32_t duration) { this->close_duration_ = duration; } diff --git a/esphome/components/total_daily_energy/total_daily_energy.h b/esphome/components/total_daily_energy/total_daily_energy.h index 1a9d5d1a49..1145f54f95 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.h +++ b/esphome/components/total_daily_energy/total_daily_energy.h @@ -23,7 +23,6 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { void set_method(TotalDailyEnergyMethod method) { method_ = method; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; void publish_state_and_save(float state); diff --git a/esphome/components/ttp229_bsf/ttp229_bsf.h b/esphome/components/ttp229_bsf/ttp229_bsf.h index 2663afcec9..fea4356b55 100644 --- a/esphome/components/ttp229_bsf/ttp229_bsf.h +++ b/esphome/components/ttp229_bsf/ttp229_bsf.h @@ -25,7 +25,6 @@ class TTP229BSFComponent : public Component { void register_channel(TTP229BSFChannel *channel) { this->channels_.push_back(channel); } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override { // check datavalid if sdo is high if (!this->sdo_pin_->digital_read()) { diff --git a/esphome/components/ttp229_lsf/ttp229_lsf.h b/esphome/components/ttp229_lsf/ttp229_lsf.h index f8775a17f0..7cc4bfca89 100644 --- a/esphome/components/ttp229_lsf/ttp229_lsf.h +++ b/esphome/components/ttp229_lsf/ttp229_lsf.h @@ -23,7 +23,6 @@ class TTP229LSFComponent : public Component, public i2c::I2CDevice { void register_channel(TTP229Channel *channel) { this->channels_.push_back(channel); } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; protected: diff --git a/esphome/components/vbus/vbus.h b/esphome/components/vbus/vbus.h index 7e97b5049a..0a253f1bdb 100644 --- a/esphome/components/vbus/vbus.h +++ b/esphome/components/vbus/vbus.h @@ -30,7 +30,6 @@ class VBus : public uart::UARTDevice, public Component { public: void dump_config() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } void register_listener(VBusListener *listener) { this->listeners_.push_back(listener); } diff --git a/esphome/components/veml3235/veml3235.h b/esphome/components/veml3235/veml3235.h index 2b0d6b23ea..b57e1571f1 100644 --- a/esphome/components/veml3235/veml3235.h +++ b/esphome/components/veml3235/veml3235.h @@ -65,7 +65,6 @@ class VEML3235Sensor : public sensor::Sensor, public PollingComponent, public i2 void setup() override; void dump_config() override; void update() override { this->publish_state(this->read_lx_()); } - float get_setup_priority() const override { return setup_priority::DATA; } // Used by ESPHome framework. Does NOT actually set the value on the device. void set_auto_gain(bool auto_gain) { this->auto_gain_ = auto_gain; } diff --git a/esphome/components/veml7700/veml7700.h b/esphome/components/veml7700/veml7700.h index 17fee6b851..b0d1451cf0 100644 --- a/esphome/components/veml7700/veml7700.h +++ b/esphome/components/veml7700/veml7700.h @@ -102,7 +102,6 @@ class VEML7700Component : public PollingComponent, public i2c::I2CDevice { // // EspHome framework functions // - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h index dd76e8e0ab..2bf90015fe 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.h +++ b/esphome/components/vl53l0x/vl53l0x_sensor.h @@ -30,7 +30,6 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void update() override; void loop() override; diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h index d05cffc4d1..393795439b 100644 --- a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h +++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h @@ -17,7 +17,6 @@ class XiaomiCGD1 : public Component, public esp32_ble_tracker::ESPBTDeviceListen bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h index 8fd9946537..1f5ef89869 100644 --- a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h +++ b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h @@ -17,7 +17,6 @@ class XiaomiCGDK2 : public Component, public esp32_ble_tracker::ESPBTDeviceListe bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h index 966c05ac79..52904fd75e 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h @@ -18,7 +18,6 @@ class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListen bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h index eff4b1c6fb..124f9411a1 100644 --- a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h +++ b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h @@ -21,7 +21,6 @@ class XiaomiCGPR1 : public Component, bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } void set_idle_time(sensor::Sensor *idle_time) { idle_time_ = idle_time; } diff --git a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h index 08e1bd7e54..83c8f15ace 100644 --- a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h +++ b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h @@ -17,7 +17,6 @@ class XiaomiGCLS002 : public Component, public esp32_ble_tracker::ESPBTDeviceLis bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } diff --git a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h index aa99cc004a..96ea9217fb 100644 --- a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h @@ -17,7 +17,6 @@ class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceL bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } diff --git a/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h index bc1e580ce4..bd4ad75c1d 100644 --- a/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h +++ b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h @@ -16,7 +16,6 @@ class XiaomiHHCCJCY10 : public Component, public esp32_ble_tracker::ESPBTDeviceL bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } void set_moisture(sensor::Sensor *moisture) { this->moisture_ = moisture; } void set_conductivity(sensor::Sensor *conductivity) { this->conductivity_ = conductivity; } diff --git a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h index ce746b9ee0..0ec34b1871 100644 --- a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h +++ b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h @@ -17,7 +17,6 @@ class XiaomiHHCCPOT002 : public Component, public esp32_ble_tracker::ESPBTDevice bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } diff --git a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h index ca1ad0f27e..e9c44800f2 100644 --- a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h +++ b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h @@ -17,7 +17,6 @@ class XiaomiJQJCY01YM : public Component, public esp32_ble_tracker::ESPBTDeviceL bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_formaldehyde(sensor::Sensor *formaldehyde) { formaldehyde_ = formaldehyde; } diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h index 641a02bd5a..772b389a92 100644 --- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h @@ -17,7 +17,6 @@ class XiaomiLYWSD02 : public Component, public esp32_ble_tracker::ESPBTDeviceLis bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h index 19092aa2a9..e1e0fcae40 100644 --- a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h +++ b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h @@ -18,7 +18,6 @@ class XiaomiLYWSD02MMC : public Component, public esp32_ble_tracker::ESPBTDevice bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h index 95710a1508..3c7907479a 100644 --- a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h @@ -17,7 +17,6 @@ class XiaomiLYWSD03MMC : public Component, public esp32_ble_tracker::ESPBTDevice bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h index cbc76f9dd3..cf90db937f 100644 --- a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h @@ -17,7 +17,6 @@ class XiaomiLYWSDCGQ : public Component, public esp32_ble_tracker::ESPBTDeviceLi bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h b/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h index d0304f7894..c3b8e7d68f 100644 --- a/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h +++ b/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h @@ -17,7 +17,6 @@ class XiaomiMHOC303 : public Component, public esp32_ble_tracker::ESPBTDeviceLis bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h index 4ab882b2af..1acdaa88af 100644 --- a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h +++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h @@ -17,7 +17,6 @@ class XiaomiMHOC401 : public Component, public esp32_ble_tracker::ESPBTDeviceLis bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.h b/esphome/components/xiaomi_miscale/xiaomi_miscale.h index 4523bbc82b..10d308ef6c 100644 --- a/esphome/components/xiaomi_miscale/xiaomi_miscale.h +++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.h @@ -23,7 +23,6 @@ class XiaomiMiscale : public Component, public esp32_ble_tracker::ESPBTDeviceLis bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_weight(sensor::Sensor *weight) { weight_ = weight; } void set_impedance(sensor::Sensor *impedance) { impedance_ = impedance; } void set_clear_impedance(bool clear_impedance) { clear_impedance_ = clear_impedance; } diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h index 34b1fe4af0..e1b4055696 100644 --- a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h +++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h @@ -21,7 +21,6 @@ class XiaomiMJYD02YLA : public Component, bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_idle_time(sensor::Sensor *idle_time) { idle_time_ = idle_time; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h index 904c575ae6..f1da0705d0 100644 --- a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h +++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h @@ -19,7 +19,6 @@ class XiaomiMUE4094RT : public Component, bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_time(uint16_t timeout) { timeout_ = timeout; } protected: diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h index a16c5209d9..ae00a28ac9 100644 --- a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h @@ -23,7 +23,6 @@ class XiaomiRTCGQ02LM : public Component, public esp32_ble_tracker::ESPBTDeviceL bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } #ifdef USE_BINARY_SENSOR void set_motion(binary_sensor::BinarySensor *motion) { this->motion_ = motion; } diff --git a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h index 297c7ab47d..081705fd50 100644 --- a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h +++ b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h @@ -20,7 +20,6 @@ class XiaomiWX08ZM : public Component, bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_tablet(sensor::Sensor *tablet) { tablet_ = tablet; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h index 9ce02bb64e..ed0458ce49 100644 --- a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h +++ b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h @@ -18,7 +18,6 @@ class XiaomiXMWSDJ04MMC : public Component, public esp32_ble_tracker::ESPBTDevic bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; } diff --git a/esphome/components/zio_ultrasonic/zio_ultrasonic.h b/esphome/components/zio_ultrasonic/zio_ultrasonic.h index 84c8d44c65..23057b2ab0 100644 --- a/esphome/components/zio_ultrasonic/zio_ultrasonic.h +++ b/esphome/components/zio_ultrasonic/zio_ultrasonic.h @@ -11,8 +11,6 @@ namespace zio_ultrasonic { class ZioUltrasonicComponent : public i2c::I2CDevice, public PollingComponent, public sensor::Sensor { public: - float get_setup_priority() const override { return setup_priority::DATA; } - void dump_config() override; void update() override; diff --git a/esphome/components/zyaura/zyaura.h b/esphome/components/zyaura/zyaura.h index 85c31ec75a..3070aa90c5 100644 --- a/esphome/components/zyaura/zyaura.h +++ b/esphome/components/zyaura/zyaura.h @@ -69,7 +69,6 @@ class ZyAuraSensor : public PollingComponent { void setup() override { this->store_.setup(this->pin_clock_, this->pin_data_); } void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: ZaSensorStore store_; From a4cc6166a0c8f014b3e821910f81a52e15070fae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 14:20:52 -0500 Subject: [PATCH 162/293] Bump aioesphomeapi from 33.1.1 to 34.0.0 (#9265) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3f306fe4fa..12f3b84359 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.9.0 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==33.1.1 +aioesphomeapi==34.0.0 zeroconf==0.147.0 puremagic==1.29 ruamel.yaml==0.18.14 # dashboard_import From b743577ebe37063992506272d74431ad8a14e0ca Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 30 Jun 2025 08:07:29 +1200 Subject: [PATCH 163/293] Fix api log client crashing when api encryption is dynamic (#9245) --- esphome/components/api/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 7f8e2529a5..2d4bc37c89 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -35,8 +35,8 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None: port: int = int(conf[CONF_PORT]) password: str = conf[CONF_PASSWORD] noise_psk: str | None = None - if CONF_ENCRYPTION in conf: - noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] + if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)): + noise_psk = key _LOGGER.info("Starting log output from %s using esphome API", address) cli = APIClient( address, From 971bbd088c9eaa0590d4ce5c51c840a246b99c3d Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Sun, 29 Jun 2025 21:34:59 +0100 Subject: [PATCH 164/293] Fix MQTT blocking main loop for multiple seconds at a time (#8325) Co-authored-by: patagona Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- .../components/mqtt/mqtt_backend_esp32.cpp | 118 +++++++++++++++++- esphome/components/mqtt/mqtt_backend_esp32.h | 110 +++++++++++++++- 2 files changed, 223 insertions(+), 5 deletions(-) diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index 64dc27d84b..62b153e676 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -6,6 +6,7 @@ #include #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace mqtt { @@ -100,9 +101,24 @@ bool MQTTBackendESP32::initialize_() { handler_.reset(mqtt_client); is_initalized_ = true; esp_mqtt_client_register_event(mqtt_client, MQTT_EVENT_ANY, mqtt_event_handler, this); +#if defined(USE_MQTT_IDF_ENQUEUE) + // Create the task only after MQTT client is initialized successfully + // Use larger stack size when TLS is enabled + size_t stack_size = this->ca_certificate_.has_value() ? TASK_STACK_SIZE_TLS : TASK_STACK_SIZE; + xTaskCreate(esphome_mqtt_task, "esphome_mqtt", stack_size, (void *) this, TASK_PRIORITY, &this->task_handle_); + if (this->task_handle_ == nullptr) { + ESP_LOGE(TAG, "Failed to create MQTT task"); + // Clean up MQTT client since we can't start the async task + handler_.reset(); + is_initalized_ = false; + return false; + } + // Set the task handle so the queue can notify it + this->mqtt_queue_.set_task_to_notify(this->task_handle_); +#endif return true; } else { - ESP_LOGE(TAG, "Failed to initialize IDF-MQTT"); + ESP_LOGE(TAG, "Failed to init client"); return false; } } @@ -115,6 +131,26 @@ void MQTTBackendESP32::loop() { mqtt_event_handler_(event); mqtt_events_.pop(); } + +#if defined(USE_MQTT_IDF_ENQUEUE) + // Periodically log dropped messages to avoid blocking during spikes. + // During high load, many messages can be dropped in quick succession. + // Logging each drop immediately would flood the logs and potentially + // cause more drops if MQTT logging is enabled (cascade effect). + // Instead, we accumulate the count and log a summary periodically. + // IMPORTANT: Don't move this to the scheduler - if drops are due to memory + // pressure, the scheduler's heap allocations would make things worse. + uint32_t now = App.get_loop_component_start_time(); + // Handle rollover: (now - last_time) works correctly with unsigned arithmetic + // even when now < last_time due to rollover + if ((now - this->last_dropped_log_time_) >= DROP_LOG_INTERVAL_MS) { + uint16_t dropped = this->mqtt_queue_.get_and_reset_dropped_count(); + if (dropped > 0) { + ESP_LOGW(TAG, "Dropped %u messages (%us)", dropped, DROP_LOG_INTERVAL_MS / 1000); + } + this->last_dropped_log_time_ = now; + } +#endif } void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { @@ -188,6 +224,86 @@ void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t b } } +#if defined(USE_MQTT_IDF_ENQUEUE) +void MQTTBackendESP32::esphome_mqtt_task(void *params) { + MQTTBackendESP32 *this_mqtt = (MQTTBackendESP32 *) params; + + while (true) { + // Wait for notification indefinitely + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + // Process all queued items + struct QueueElement *elem; + while ((elem = this_mqtt->mqtt_queue_.pop()) != nullptr) { + if (this_mqtt->is_connected_) { + switch (elem->type) { + case MQTT_QUEUE_TYPE_SUBSCRIBE: + esp_mqtt_client_subscribe(this_mqtt->handler_.get(), elem->topic, elem->qos); + break; + + case MQTT_QUEUE_TYPE_UNSUBSCRIBE: + esp_mqtt_client_unsubscribe(this_mqtt->handler_.get(), elem->topic); + break; + + case MQTT_QUEUE_TYPE_PUBLISH: + esp_mqtt_client_publish(this_mqtt->handler_.get(), elem->topic, elem->payload, elem->payload_len, elem->qos, + elem->retain); + break; + + default: + ESP_LOGE(TAG, "Invalid operation type from MQTT queue"); + break; + } + } + this_mqtt->mqtt_event_pool_.release(elem); + } + } + + // Clean up any remaining items in the queue + struct QueueElement *elem; + while ((elem = this_mqtt->mqtt_queue_.pop()) != nullptr) { + this_mqtt->mqtt_event_pool_.release(elem); + } + + // Note: EventPool destructor will clean up the pool itself + // Task will delete itself + vTaskDelete(nullptr); +} + +bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos, bool retain, const char *payload, + size_t len) { + auto *elem = this->mqtt_event_pool_.allocate(); + + if (!elem) { + // Queue is full - increment counter but don't log immediately. + // Logging here can cause a cascade effect: if MQTT logging is enabled, + // each dropped message would generate a log message, which could itself + // be sent via MQTT, causing more drops and more logs in a feedback loop + // that eventually triggers a watchdog reset. Instead, we log periodically + // in loop() to prevent blocking the event loop during spikes. + this->mqtt_queue_.increment_dropped_count(); + return false; + } + + elem->type = type; + elem->qos = qos; + elem->retain = retain; + + // Use the helper to allocate and copy data + if (!elem->set_data(topic, payload, len)) { + // Allocation failed, return elem to pool + this->mqtt_event_pool_.release(elem); + // Increment counter without logging to avoid cascade effect during memory pressure + this->mqtt_queue_.increment_dropped_count(); + return false; + } + + // Push to queue - always succeeds since we allocated from the pool + this->mqtt_queue_.push(elem); + return true; +} +#endif // USE_MQTT_IDF_ENQUEUE + } // namespace mqtt } // namespace esphome #endif // USE_ESP32 diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index 9054702115..57286a24b2 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -6,9 +6,14 @@ #include #include +#include #include +#include +#include #include "esphome/components/network/ip_address.h" #include "esphome/core/helpers.h" +#include "esphome/core/lock_free_queue.h" +#include "esphome/core/event_pool.h" namespace esphome { namespace mqtt { @@ -42,9 +47,79 @@ struct Event { error_handle(*event.error_handle) {} }; +enum MqttQueueTypeT : uint8_t { + MQTT_QUEUE_TYPE_NONE = 0, + MQTT_QUEUE_TYPE_SUBSCRIBE, + MQTT_QUEUE_TYPE_UNSUBSCRIBE, + MQTT_QUEUE_TYPE_PUBLISH, +}; + +struct QueueElement { + char *topic; + char *payload; + uint16_t payload_len; // MQTT max payload is 64KiB + uint8_t type : 2; + uint8_t qos : 2; // QoS only needs values 0-2 + uint8_t retain : 1; + uint8_t reserved : 3; // Reserved for future use + + QueueElement() : topic(nullptr), payload(nullptr), payload_len(0), qos(0), retain(0), reserved(0) {} + + // Helper to set topic/payload (uses RAMAllocator) + bool set_data(const char *topic_str, const char *payload_data, size_t len) { + // Check payload size limit (MQTT max is 64KiB) + if (len > std::numeric_limits::max()) { + return false; + } + + // Use RAMAllocator with default flags (tries external RAM first, falls back to internal) + RAMAllocator allocator; + + // Allocate and copy topic + size_t topic_len = strlen(topic_str) + 1; + topic = allocator.allocate(topic_len); + if (!topic) + return false; + memcpy(topic, topic_str, topic_len); + + if (payload_data && len) { + payload = allocator.allocate(len); + if (!payload) { + allocator.deallocate(topic, topic_len); + topic = nullptr; + return false; + } + memcpy(payload, payload_data, len); + payload_len = static_cast(len); + } else { + payload = nullptr; + payload_len = 0; + } + return true; + } + + // Helper to release (uses RAMAllocator) + void release() { + RAMAllocator allocator; + if (topic) { + allocator.deallocate(topic, strlen(topic) + 1); + topic = nullptr; + } + if (payload) { + allocator.deallocate(payload, payload_len); + payload = nullptr; + } + payload_len = 0; + } +}; + class MQTTBackendESP32 final : public MQTTBackend { public: static const size_t MQTT_BUFFER_SIZE = 4096; + static const size_t TASK_STACK_SIZE = 2048; + static const size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations + static const ssize_t TASK_PRIORITY = 5; + static const uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; } void set_client_id(const char *client_id) final { this->client_id_ = client_id; } @@ -105,15 +180,23 @@ class MQTTBackendESP32 final : public MQTTBackend { } bool subscribe(const char *topic, uint8_t qos) final { +#if defined(USE_MQTT_IDF_ENQUEUE) + return enqueue_(MQTT_QUEUE_TYPE_SUBSCRIBE, topic, qos); +#else return esp_mqtt_client_subscribe(handler_.get(), topic, qos) != -1; +#endif + } + bool unsubscribe(const char *topic) final { +#if defined(USE_MQTT_IDF_ENQUEUE) + return enqueue_(MQTT_QUEUE_TYPE_UNSUBSCRIBE, topic); +#else + return esp_mqtt_client_unsubscribe(handler_.get(), topic) != -1; +#endif } - bool unsubscribe(const char *topic) final { return esp_mqtt_client_unsubscribe(handler_.get(), topic) != -1; } bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) final { #if defined(USE_MQTT_IDF_ENQUEUE) - // use the non-blocking version - // it can delay sending a couple of seconds but won't block - return esp_mqtt_client_enqueue(handler_.get(), topic, payload, length, qos, retain, true) != -1; + return enqueue_(MQTT_QUEUE_TYPE_PUBLISH, topic, qos, retain, payload, length); #else // might block for several seconds, either due to network timeout (10s) // or if publishing payloads longer than internal buffer (due to message fragmentation) @@ -129,6 +212,12 @@ class MQTTBackendESP32 final : public MQTTBackend { void set_cl_key(const std::string &key) { cl_key_ = key; } void set_skip_cert_cn_check(bool skip_check) { skip_cert_cn_check_ = skip_check; } + // No destructor needed: ESPHome components live for the entire device runtime. + // The MQTT task and queue will run until the device reboots or loses power, + // at which point the entire process terminates and FreeRTOS cleans up all tasks. + // Implementing a destructor would add complexity and potential race conditions + // for a scenario that never occurs in practice. + protected: bool initialize_(); void mqtt_event_handler_(const Event &event); @@ -160,6 +249,14 @@ class MQTTBackendESP32 final : public MQTTBackend { optional cl_certificate_; optional cl_key_; bool skip_cert_cn_check_{false}; +#if defined(USE_MQTT_IDF_ENQUEUE) + static void esphome_mqtt_task(void *params); + EventPool mqtt_event_pool_; + LockFreeQueue mqtt_queue_; + TaskHandle_t task_handle_{nullptr}; + bool enqueue_(MqttQueueTypeT type, const char *topic, int qos = 0, bool retain = false, const char *payload = NULL, + size_t len = 0); +#endif // callbacks CallbackManager on_connect_; @@ -169,6 +266,11 @@ class MQTTBackendESP32 final : public MQTTBackend { CallbackManager on_message_; CallbackManager on_publish_; std::queue mqtt_events_; + +#if defined(USE_MQTT_IDF_ENQUEUE) + uint32_t last_dropped_log_time_{0}; + static constexpr uint32_t DROP_LOG_INTERVAL_MS = 10000; // Log every 10 seconds +#endif }; } // namespace mqtt From d592208c74d80e573471774f6401b8244effd5b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 17:45:41 -0500 Subject: [PATCH 165/293] Fix crash when event last_event_type is null in web_server (#9266) --- esphome/components/web_server/web_server.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 9f42253794..927659e621 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1683,12 +1683,15 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa request->send(404); } +static std::string get_event_type(event::Event *event) { return event->last_event_type ? *event->last_event_type : ""; } + std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { - return web_server->event_json((event::Event *) (source), *(((event::Event *) (source))->last_event_type), - DETAIL_STATE); + auto *event = static_cast(source); + return web_server->event_json(event, get_event_type(event), DETAIL_STATE); } std::string WebServer::event_all_json_generator(WebServer *web_server, void *source) { - return web_server->event_json((event::Event *) (source), *(((event::Event *) (source))->last_event_type), DETAIL_ALL); + auto *event = static_cast(source); + return web_server->event_json(event, get_event_type(event), DETAIL_ALL); } std::string WebServer::event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config) { return json::build_json([this, obj, event_type, start_config](JsonObject root) { From d78b7203503012529680e624efcc7573b366ab41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:38:11 -0500 Subject: [PATCH 166/293] Remove single-use send_*_info wrappers in API connection (#9255) --- esphome/components/api/api_connection.cpp | 70 --------- esphome/components/api/api_connection.h | 23 +-- esphome/components/api/list_entities.cpp | 173 +++++++-------------- esphome/components/api/list_entities.h | 52 ++++--- esphome/components/api/subscribe_state.cpp | 52 +++---- esphome/components/api/subscribe_state.h | 45 +++--- 6 files changed, 132 insertions(+), 283 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index f339a4b26f..8550d45bfc 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -304,10 +304,6 @@ bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary return this->schedule_message_(binary_sensor, &APIConnection::try_send_binary_sensor_state, BinarySensorStateResponse::MESSAGE_TYPE); } -void APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor) { - this->schedule_message_(binary_sensor, &APIConnection::try_send_binary_sensor_info, - ListEntitiesBinarySensorResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -335,9 +331,6 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne bool APIConnection::send_cover_state(cover::Cover *cover) { return this->schedule_message_(cover, &APIConnection::try_send_cover_state, CoverStateResponse::MESSAGE_TYPE); } -void APIConnection::send_cover_info(cover::Cover *cover) { - this->schedule_message_(cover, &APIConnection::try_send_cover_info, ListEntitiesCoverResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *cover = static_cast(entity); @@ -399,9 +392,6 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) { bool APIConnection::send_fan_state(fan::Fan *fan) { return this->schedule_message_(fan, &APIConnection::try_send_fan_state, FanStateResponse::MESSAGE_TYPE); } -void APIConnection::send_fan_info(fan::Fan *fan) { - this->schedule_message_(fan, &APIConnection::try_send_fan_info, ListEntitiesFanResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *fan = static_cast(entity); @@ -461,9 +451,6 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { bool APIConnection::send_light_state(light::LightState *light) { return this->schedule_message_(light, &APIConnection::try_send_light_state, LightStateResponse::MESSAGE_TYPE); } -void APIConnection::send_light_info(light::LightState *light) { - this->schedule_message_(light, &APIConnection::try_send_light_info, ListEntitiesLightResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *light = static_cast(entity); @@ -556,9 +543,6 @@ void APIConnection::light_command(const LightCommandRequest &msg) { bool APIConnection::send_sensor_state(sensor::Sensor *sensor) { return this->schedule_message_(sensor, &APIConnection::try_send_sensor_state, SensorStateResponse::MESSAGE_TYPE); } -void APIConnection::send_sensor_info(sensor::Sensor *sensor) { - this->schedule_message_(sensor, &APIConnection::try_send_sensor_info, ListEntitiesSensorResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -591,9 +575,6 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection * bool APIConnection::send_switch_state(switch_::Switch *a_switch) { return this->schedule_message_(a_switch, &APIConnection::try_send_switch_state, SwitchStateResponse::MESSAGE_TYPE); } -void APIConnection::send_switch_info(switch_::Switch *a_switch) { - this->schedule_message_(a_switch, &APIConnection::try_send_switch_info, ListEntitiesSwitchResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -632,10 +613,6 @@ bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) return this->schedule_message_(text_sensor, &APIConnection::try_send_text_sensor_state, TextSensorStateResponse::MESSAGE_TYPE); } -void APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) { - this->schedule_message_(text_sensor, &APIConnection::try_send_text_sensor_info, - ListEntitiesTextSensorResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -696,9 +673,6 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection resp.target_humidity = climate->target_humidity; return encode_message_to_buffer(resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_climate_info(climate::Climate *climate) { - this->schedule_message_(climate, &APIConnection::try_send_climate_info, ListEntitiesClimateResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *climate = static_cast(entity); @@ -766,9 +740,6 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { bool APIConnection::send_number_state(number::Number *number) { return this->schedule_message_(number, &APIConnection::try_send_number_state, NumberStateResponse::MESSAGE_TYPE); } -void APIConnection::send_number_info(number::Number *number) { - this->schedule_message_(number, &APIConnection::try_send_number_info, ListEntitiesNumberResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -820,9 +791,6 @@ uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *c fill_entity_state_base(date, resp); return encode_message_to_buffer(resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_date_info(datetime::DateEntity *date) { - this->schedule_message_(date, &APIConnection::try_send_date_info, ListEntitiesDateResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *date = static_cast(entity); @@ -857,9 +825,6 @@ uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *c fill_entity_state_base(time, resp); return encode_message_to_buffer(resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_time_info(datetime::TimeEntity *time) { - this->schedule_message_(time, &APIConnection::try_send_time_info, ListEntitiesTimeResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *time = static_cast(entity); @@ -896,9 +861,6 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio fill_entity_state_base(datetime, resp); return encode_message_to_buffer(resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) { - this->schedule_message_(datetime, &APIConnection::try_send_datetime_info, ListEntitiesDateTimeResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *datetime = static_cast(entity); @@ -922,9 +884,6 @@ void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { bool APIConnection::send_text_state(text::Text *text) { return this->schedule_message_(text, &APIConnection::try_send_text_state, TextStateResponse::MESSAGE_TYPE); } -void APIConnection::send_text_info(text::Text *text) { - this->schedule_message_(text, &APIConnection::try_send_text_info, ListEntitiesTextResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -963,9 +922,6 @@ void APIConnection::text_command(const TextCommandRequest &msg) { bool APIConnection::send_select_state(select::Select *select) { return this->schedule_message_(select, &APIConnection::try_send_select_state, SelectStateResponse::MESSAGE_TYPE); } -void APIConnection::send_select_info(select::Select *select) { - this->schedule_message_(select, &APIConnection::try_send_select_info, ListEntitiesSelectResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -999,9 +955,6 @@ void APIConnection::select_command(const SelectCommandRequest &msg) { #endif #ifdef USE_BUTTON -void esphome::api::APIConnection::send_button_info(button::Button *button) { - this->schedule_message_(button, &APIConnection::try_send_button_info, ListEntitiesButtonResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *button = static_cast(entity); @@ -1024,9 +977,6 @@ void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg bool APIConnection::send_lock_state(lock::Lock *a_lock) { return this->schedule_message_(a_lock, &APIConnection::try_send_lock_state, LockStateResponse::MESSAGE_TYPE); } -void APIConnection::send_lock_info(lock::Lock *a_lock) { - this->schedule_message_(a_lock, &APIConnection::try_send_lock_info, ListEntitiesLockResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -1080,9 +1030,6 @@ uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection * fill_entity_state_base(valve, resp); return encode_message_to_buffer(resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_valve_info(valve::Valve *valve) { - this->schedule_message_(valve, &APIConnection::try_send_valve_info, ListEntitiesValveResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *valve = static_cast(entity); @@ -1128,10 +1075,6 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne fill_entity_state_base(media_player, resp); return encode_message_to_buffer(resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) { - this->schedule_message_(media_player, &APIConnection::try_send_media_player_info, - ListEntitiesMediaPlayerResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *media_player = static_cast(entity); @@ -1183,9 +1126,6 @@ void APIConnection::set_camera_state(std::shared_ptr image->was_requested_by(esphome::esp32_camera::IDLE)) this->image_reader_.set_image(std::move(image)); } -void APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { - this->schedule_message_(camera, &APIConnection::try_send_camera_info, ListEntitiesCameraResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *camera = static_cast(entity); @@ -1392,10 +1332,6 @@ uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, A fill_entity_state_base(a_alarm_control_panel, resp); return encode_message_to_buffer(resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { - this->schedule_message_(a_alarm_control_panel, &APIConnection::try_send_alarm_control_panel_info, - ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *a_alarm_control_panel = static_cast(entity); @@ -1446,9 +1382,6 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe void APIConnection::send_event(event::Event *event, const std::string &event_type) { this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE); } -void APIConnection::send_event_info(event::Event *event) { - this->schedule_message_(event, &APIConnection::try_send_event_info, ListEntitiesEventResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { EventResponse resp; @@ -1494,9 +1427,6 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection fill_entity_state_base(update, resp); return encode_message_to_buffer(resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_update_info(update::UpdateEntity *update) { - this->schedule_message_(update, &APIConnection::try_send_update_info, ListEntitiesUpdateResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *update = static_cast(entity); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 4397462d8e..c9f24a7759 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -22,6 +22,7 @@ static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; class APIConnection : public APIServerConnection { public: friend class APIServer; + friend class ListEntitiesIterator; APIConnection(std::unique_ptr socket, APIServer *parent); virtual ~APIConnection(); @@ -34,93 +35,74 @@ class APIConnection : public APIServerConnection { } #ifdef USE_BINARY_SENSOR bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor); - void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor); #endif #ifdef USE_COVER bool send_cover_state(cover::Cover *cover); - void send_cover_info(cover::Cover *cover); void cover_command(const CoverCommandRequest &msg) override; #endif #ifdef USE_FAN bool send_fan_state(fan::Fan *fan); - void send_fan_info(fan::Fan *fan); void fan_command(const FanCommandRequest &msg) override; #endif #ifdef USE_LIGHT bool send_light_state(light::LightState *light); - void send_light_info(light::LightState *light); void light_command(const LightCommandRequest &msg) override; #endif #ifdef USE_SENSOR bool send_sensor_state(sensor::Sensor *sensor); - void send_sensor_info(sensor::Sensor *sensor); #endif #ifdef USE_SWITCH bool send_switch_state(switch_::Switch *a_switch); - void send_switch_info(switch_::Switch *a_switch); void switch_command(const SwitchCommandRequest &msg) override; #endif #ifdef USE_TEXT_SENSOR bool send_text_sensor_state(text_sensor::TextSensor *text_sensor); - void send_text_sensor_info(text_sensor::TextSensor *text_sensor); #endif #ifdef USE_ESP32_CAMERA void set_camera_state(std::shared_ptr image); - void send_camera_info(esp32_camera::ESP32Camera *camera); void camera_image(const CameraImageRequest &msg) override; #endif #ifdef USE_CLIMATE bool send_climate_state(climate::Climate *climate); - void send_climate_info(climate::Climate *climate); void climate_command(const ClimateCommandRequest &msg) override; #endif #ifdef USE_NUMBER bool send_number_state(number::Number *number); - void send_number_info(number::Number *number); void number_command(const NumberCommandRequest &msg) override; #endif #ifdef USE_DATETIME_DATE bool send_date_state(datetime::DateEntity *date); - void send_date_info(datetime::DateEntity *date); void date_command(const DateCommandRequest &msg) override; #endif #ifdef USE_DATETIME_TIME bool send_time_state(datetime::TimeEntity *time); - void send_time_info(datetime::TimeEntity *time); void time_command(const TimeCommandRequest &msg) override; #endif #ifdef USE_DATETIME_DATETIME bool send_datetime_state(datetime::DateTimeEntity *datetime); - void send_datetime_info(datetime::DateTimeEntity *datetime); void datetime_command(const DateTimeCommandRequest &msg) override; #endif #ifdef USE_TEXT bool send_text_state(text::Text *text); - void send_text_info(text::Text *text); void text_command(const TextCommandRequest &msg) override; #endif #ifdef USE_SELECT bool send_select_state(select::Select *select); - void send_select_info(select::Select *select); void select_command(const SelectCommandRequest &msg) override; #endif #ifdef USE_BUTTON - void send_button_info(button::Button *button); void button_command(const ButtonCommandRequest &msg) override; #endif #ifdef USE_LOCK bool send_lock_state(lock::Lock *a_lock); - void send_lock_info(lock::Lock *a_lock); void lock_command(const LockCommandRequest &msg) override; #endif #ifdef USE_VALVE bool send_valve_state(valve::Valve *valve); - void send_valve_info(valve::Valve *valve); void valve_command(const ValveCommandRequest &msg) override; #endif #ifdef USE_MEDIA_PLAYER bool send_media_player_state(media_player::MediaPlayer *media_player); - void send_media_player_info(media_player::MediaPlayer *media_player); void media_player_command(const MediaPlayerCommandRequest &msg) override; #endif bool try_send_log_message(int level, const char *tag, const char *line); @@ -167,18 +149,15 @@ class APIConnection : public APIServerConnection { #ifdef USE_ALARM_CONTROL_PANEL bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); - void send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; #endif #ifdef USE_EVENT void send_event(event::Event *event, const std::string &event_type); - void send_event_info(event::Event *event); #endif #ifdef USE_UPDATE bool send_update_state(update::UpdateEntity *update); - void send_update_info(update::UpdateEntity *update); void update_command(const UpdateCommandRequest &msg) override; #endif diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index ceee3f00b8..3f84ef306e 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -1,6 +1,7 @@ #include "list_entities.h" #ifdef USE_API #include "api_connection.h" +#include "api_pb2.h" #include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/core/util.h" @@ -8,155 +9,85 @@ namespace esphome { namespace api { +// Generate entity handler implementations using macros #ifdef USE_BINARY_SENSOR -bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { - this->client_->send_binary_sensor_info(binary_sensor); - return true; -} +LIST_ENTITIES_HANDLER(binary_sensor, binary_sensor::BinarySensor, ListEntitiesBinarySensorResponse) #endif #ifdef USE_COVER -bool ListEntitiesIterator::on_cover(cover::Cover *cover) { - this->client_->send_cover_info(cover); - return true; -} +LIST_ENTITIES_HANDLER(cover, cover::Cover, ListEntitiesCoverResponse) #endif #ifdef USE_FAN -bool ListEntitiesIterator::on_fan(fan::Fan *fan) { - this->client_->send_fan_info(fan); - return true; -} +LIST_ENTITIES_HANDLER(fan, fan::Fan, ListEntitiesFanResponse) #endif #ifdef USE_LIGHT -bool ListEntitiesIterator::on_light(light::LightState *light) { - this->client_->send_light_info(light); - return true; -} +LIST_ENTITIES_HANDLER(light, light::LightState, ListEntitiesLightResponse) #endif #ifdef USE_SENSOR -bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { - this->client_->send_sensor_info(sensor); - return true; -} +LIST_ENTITIES_HANDLER(sensor, sensor::Sensor, ListEntitiesSensorResponse) #endif #ifdef USE_SWITCH -bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { - this->client_->send_switch_info(a_switch); - return true; -} +LIST_ENTITIES_HANDLER(switch, switch_::Switch, ListEntitiesSwitchResponse) #endif #ifdef USE_BUTTON -bool ListEntitiesIterator::on_button(button::Button *button) { - this->client_->send_button_info(button); - return true; -} +LIST_ENTITIES_HANDLER(button, button::Button, ListEntitiesButtonResponse) #endif #ifdef USE_TEXT_SENSOR -bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { - this->client_->send_text_sensor_info(text_sensor); - return true; -} +LIST_ENTITIES_HANDLER(text_sensor, text_sensor::TextSensor, ListEntitiesTextSensorResponse) #endif #ifdef USE_LOCK -bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { - this->client_->send_lock_info(a_lock); - return true; -} +LIST_ENTITIES_HANDLER(lock, lock::Lock, ListEntitiesLockResponse) #endif #ifdef USE_VALVE -bool ListEntitiesIterator::on_valve(valve::Valve *valve) { - this->client_->send_valve_info(valve); - return true; -} +LIST_ENTITIES_HANDLER(valve, valve::Valve, ListEntitiesValveResponse) +#endif +#ifdef USE_ESP32_CAMERA +LIST_ENTITIES_HANDLER(camera, esp32_camera::ESP32Camera, ListEntitiesCameraResponse) +#endif +#ifdef USE_CLIMATE +LIST_ENTITIES_HANDLER(climate, climate::Climate, ListEntitiesClimateResponse) +#endif +#ifdef USE_NUMBER +LIST_ENTITIES_HANDLER(number, number::Number, ListEntitiesNumberResponse) +#endif +#ifdef USE_DATETIME_DATE +LIST_ENTITIES_HANDLER(date, datetime::DateEntity, ListEntitiesDateResponse) +#endif +#ifdef USE_DATETIME_TIME +LIST_ENTITIES_HANDLER(time, datetime::TimeEntity, ListEntitiesTimeResponse) +#endif +#ifdef USE_DATETIME_DATETIME +LIST_ENTITIES_HANDLER(datetime, datetime::DateTimeEntity, ListEntitiesDateTimeResponse) +#endif +#ifdef USE_TEXT +LIST_ENTITIES_HANDLER(text, text::Text, ListEntitiesTextResponse) +#endif +#ifdef USE_SELECT +LIST_ENTITIES_HANDLER(select, select::Select, ListEntitiesSelectResponse) +#endif +#ifdef USE_MEDIA_PLAYER +LIST_ENTITIES_HANDLER(media_player, media_player::MediaPlayer, ListEntitiesMediaPlayerResponse) +#endif +#ifdef USE_ALARM_CONTROL_PANEL +LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel, + ListEntitiesAlarmControlPanelResponse) +#endif +#ifdef USE_EVENT +LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse) +#endif +#ifdef USE_UPDATE +LIST_ENTITIES_HANDLER(update, update::UpdateEntity, ListEntitiesUpdateResponse) #endif +// Special cases that don't follow the pattern bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); } + ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} + bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { auto resp = service->encode_list_service_response(); return this->client_->send_message(resp); } -#ifdef USE_ESP32_CAMERA -bool ListEntitiesIterator::on_camera(esp32_camera::ESP32Camera *camera) { - this->client_->send_camera_info(camera); - return true; -} -#endif - -#ifdef USE_CLIMATE -bool ListEntitiesIterator::on_climate(climate::Climate *climate) { - this->client_->send_climate_info(climate); - return true; -} -#endif - -#ifdef USE_NUMBER -bool ListEntitiesIterator::on_number(number::Number *number) { - this->client_->send_number_info(number); - return true; -} -#endif - -#ifdef USE_DATETIME_DATE -bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { - this->client_->send_date_info(date); - return true; -} -#endif - -#ifdef USE_DATETIME_TIME -bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { - this->client_->send_time_info(time); - return true; -} -#endif - -#ifdef USE_DATETIME_DATETIME -bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) { - this->client_->send_datetime_info(datetime); - return true; -} -#endif - -#ifdef USE_TEXT -bool ListEntitiesIterator::on_text(text::Text *text) { - this->client_->send_text_info(text); - return true; -} -#endif - -#ifdef USE_SELECT -bool ListEntitiesIterator::on_select(select::Select *select) { - this->client_->send_select_info(select); - return true; -} -#endif - -#ifdef USE_MEDIA_PLAYER -bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_player) { - this->client_->send_media_player_info(media_player); - return true; -} -#endif -#ifdef USE_ALARM_CONTROL_PANEL -bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { - this->client_->send_alarm_control_panel_info(a_alarm_control_panel); - return true; -} -#endif -#ifdef USE_EVENT -bool ListEntitiesIterator::on_event(event::Event *event) { - this->client_->send_event_info(event); - return true; -} -#endif -#ifdef USE_UPDATE -bool ListEntitiesIterator::on_update(update::UpdateEntity *update) { - this->client_->send_update_info(update); - return true; -} -#endif - } // namespace api } // namespace esphome #endif diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index e77f21c7a1..b9506073d2 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -9,75 +9,83 @@ namespace api { class APIConnection; +// Macro for generating ListEntitiesIterator handlers +// Calls schedule_message_ with try_send_*_info +#define LIST_ENTITIES_HANDLER(entity_type, EntityClass, ResponseType) \ + bool ListEntitiesIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \ + return this->client_->schedule_message_(entity, &APIConnection::try_send_##entity_type##_info, \ + ResponseType::MESSAGE_TYPE); \ + } + class ListEntitiesIterator : public ComponentIterator { public: ListEntitiesIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR - bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; + bool on_binary_sensor(binary_sensor::BinarySensor *entity) override; #endif #ifdef USE_COVER - bool on_cover(cover::Cover *cover) override; + bool on_cover(cover::Cover *entity) override; #endif #ifdef USE_FAN - bool on_fan(fan::Fan *fan) override; + bool on_fan(fan::Fan *entity) override; #endif #ifdef USE_LIGHT - bool on_light(light::LightState *light) override; + bool on_light(light::LightState *entity) override; #endif #ifdef USE_SENSOR - bool on_sensor(sensor::Sensor *sensor) override; + bool on_sensor(sensor::Sensor *entity) override; #endif #ifdef USE_SWITCH - bool on_switch(switch_::Switch *a_switch) override; + bool on_switch(switch_::Switch *entity) override; #endif #ifdef USE_BUTTON - bool on_button(button::Button *button) override; + bool on_button(button::Button *entity) override; #endif #ifdef USE_TEXT_SENSOR - bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; + bool on_text_sensor(text_sensor::TextSensor *entity) override; #endif bool on_service(UserServiceDescriptor *service) override; #ifdef USE_ESP32_CAMERA - bool on_camera(esp32_camera::ESP32Camera *camera) override; + bool on_camera(esp32_camera::ESP32Camera *entity) override; #endif #ifdef USE_CLIMATE - bool on_climate(climate::Climate *climate) override; + bool on_climate(climate::Climate *entity) override; #endif #ifdef USE_NUMBER - bool on_number(number::Number *number) override; + bool on_number(number::Number *entity) override; #endif #ifdef USE_DATETIME_DATE - bool on_date(datetime::DateEntity *date) override; + bool on_date(datetime::DateEntity *entity) override; #endif #ifdef USE_DATETIME_TIME - bool on_time(datetime::TimeEntity *time) override; + bool on_time(datetime::TimeEntity *entity) override; #endif #ifdef USE_DATETIME_DATETIME - bool on_datetime(datetime::DateTimeEntity *datetime) override; + bool on_datetime(datetime::DateTimeEntity *entity) override; #endif #ifdef USE_TEXT - bool on_text(text::Text *text) override; + bool on_text(text::Text *entity) override; #endif #ifdef USE_SELECT - bool on_select(select::Select *select) override; + bool on_select(select::Select *entity) override; #endif #ifdef USE_LOCK - bool on_lock(lock::Lock *a_lock) override; + bool on_lock(lock::Lock *entity) override; #endif #ifdef USE_VALVE - bool on_valve(valve::Valve *valve) override; + bool on_valve(valve::Valve *entity) override; #endif #ifdef USE_MEDIA_PLAYER - bool on_media_player(media_player::MediaPlayer *media_player) override; + bool on_media_player(media_player::MediaPlayer *entity) override; #endif #ifdef USE_ALARM_CONTROL_PANEL - bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; + bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override; #endif #ifdef USE_EVENT - bool on_event(event::Event *event) override; + bool on_event(event::Event *entity) override; #endif #ifdef USE_UPDATE - bool on_update(update::UpdateEntity *update) override; + bool on_update(update::UpdateEntity *entity) override; #endif bool on_end() override; bool completed() { return this->state_ == IteratorState::NONE; } diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 4180435fcc..12accf4613 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -6,73 +6,67 @@ namespace esphome { namespace api { +// Generate entity handler implementations using macros #ifdef USE_BINARY_SENSOR -bool InitialStateIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { - return this->client_->send_binary_sensor_state(binary_sensor); -} +INITIAL_STATE_HANDLER(binary_sensor, binary_sensor::BinarySensor) #endif #ifdef USE_COVER -bool InitialStateIterator::on_cover(cover::Cover *cover) { return this->client_->send_cover_state(cover); } +INITIAL_STATE_HANDLER(cover, cover::Cover) #endif #ifdef USE_FAN -bool InitialStateIterator::on_fan(fan::Fan *fan) { return this->client_->send_fan_state(fan); } +INITIAL_STATE_HANDLER(fan, fan::Fan) #endif #ifdef USE_LIGHT -bool InitialStateIterator::on_light(light::LightState *light) { return this->client_->send_light_state(light); } +INITIAL_STATE_HANDLER(light, light::LightState) #endif #ifdef USE_SENSOR -bool InitialStateIterator::on_sensor(sensor::Sensor *sensor) { return this->client_->send_sensor_state(sensor); } +INITIAL_STATE_HANDLER(sensor, sensor::Sensor) #endif #ifdef USE_SWITCH -bool InitialStateIterator::on_switch(switch_::Switch *a_switch) { return this->client_->send_switch_state(a_switch); } +INITIAL_STATE_HANDLER(switch, switch_::Switch) #endif #ifdef USE_TEXT_SENSOR -bool InitialStateIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { - return this->client_->send_text_sensor_state(text_sensor); -} +INITIAL_STATE_HANDLER(text_sensor, text_sensor::TextSensor) #endif #ifdef USE_CLIMATE -bool InitialStateIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_state(climate); } +INITIAL_STATE_HANDLER(climate, climate::Climate) #endif #ifdef USE_NUMBER -bool InitialStateIterator::on_number(number::Number *number) { return this->client_->send_number_state(number); } +INITIAL_STATE_HANDLER(number, number::Number) #endif #ifdef USE_DATETIME_DATE -bool InitialStateIterator::on_date(datetime::DateEntity *date) { return this->client_->send_date_state(date); } +INITIAL_STATE_HANDLER(date, datetime::DateEntity) #endif #ifdef USE_DATETIME_TIME -bool InitialStateIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_state(time); } +INITIAL_STATE_HANDLER(time, datetime::TimeEntity) #endif #ifdef USE_DATETIME_DATETIME -bool InitialStateIterator::on_datetime(datetime::DateTimeEntity *datetime) { - return this->client_->send_datetime_state(datetime); -} +INITIAL_STATE_HANDLER(datetime, datetime::DateTimeEntity) #endif #ifdef USE_TEXT -bool InitialStateIterator::on_text(text::Text *text) { return this->client_->send_text_state(text); } +INITIAL_STATE_HANDLER(text, text::Text) #endif #ifdef USE_SELECT -bool InitialStateIterator::on_select(select::Select *select) { return this->client_->send_select_state(select); } +INITIAL_STATE_HANDLER(select, select::Select) #endif #ifdef USE_LOCK -bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock); } +INITIAL_STATE_HANDLER(lock, lock::Lock) #endif #ifdef USE_VALVE -bool InitialStateIterator::on_valve(valve::Valve *valve) { return this->client_->send_valve_state(valve); } +INITIAL_STATE_HANDLER(valve, valve::Valve) #endif #ifdef USE_MEDIA_PLAYER -bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_player) { - return this->client_->send_media_player_state(media_player); -} +INITIAL_STATE_HANDLER(media_player, media_player::MediaPlayer) #endif #ifdef USE_ALARM_CONTROL_PANEL -bool InitialStateIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { - return this->client_->send_alarm_control_panel_state(a_alarm_control_panel); -} +INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel) #endif #ifdef USE_UPDATE -bool InitialStateIterator::on_update(update::UpdateEntity *update) { return this->client_->send_update_state(update); } +INITIAL_STATE_HANDLER(update, update::UpdateEntity) #endif + +// Special cases (button and event) are already defined inline in subscribe_state.h + InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} } // namespace api diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 3966c97af5..2b7b508056 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -10,71 +10,78 @@ namespace api { class APIConnection; +// Macro for generating InitialStateIterator handlers +// Calls send_*_state +#define INITIAL_STATE_HANDLER(entity_type, EntityClass) \ + bool InitialStateIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \ + return this->client_->send_##entity_type##_state(entity); \ + } + class InitialStateIterator : public ComponentIterator { public: InitialStateIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR - bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; + bool on_binary_sensor(binary_sensor::BinarySensor *entity) override; #endif #ifdef USE_COVER - bool on_cover(cover::Cover *cover) override; + bool on_cover(cover::Cover *entity) override; #endif #ifdef USE_FAN - bool on_fan(fan::Fan *fan) override; + bool on_fan(fan::Fan *entity) override; #endif #ifdef USE_LIGHT - bool on_light(light::LightState *light) override; + bool on_light(light::LightState *entity) override; #endif #ifdef USE_SENSOR - bool on_sensor(sensor::Sensor *sensor) override; + bool on_sensor(sensor::Sensor *entity) override; #endif #ifdef USE_SWITCH - bool on_switch(switch_::Switch *a_switch) override; + bool on_switch(switch_::Switch *entity) override; #endif #ifdef USE_BUTTON bool on_button(button::Button *button) override { return true; }; #endif #ifdef USE_TEXT_SENSOR - bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; + bool on_text_sensor(text_sensor::TextSensor *entity) override; #endif #ifdef USE_CLIMATE - bool on_climate(climate::Climate *climate) override; + bool on_climate(climate::Climate *entity) override; #endif #ifdef USE_NUMBER - bool on_number(number::Number *number) override; + bool on_number(number::Number *entity) override; #endif #ifdef USE_DATETIME_DATE - bool on_date(datetime::DateEntity *date) override; + bool on_date(datetime::DateEntity *entity) override; #endif #ifdef USE_DATETIME_TIME - bool on_time(datetime::TimeEntity *time) override; + bool on_time(datetime::TimeEntity *entity) override; #endif #ifdef USE_DATETIME_DATETIME - bool on_datetime(datetime::DateTimeEntity *datetime) override; + bool on_datetime(datetime::DateTimeEntity *entity) override; #endif #ifdef USE_TEXT - bool on_text(text::Text *text) override; + bool on_text(text::Text *entity) override; #endif #ifdef USE_SELECT - bool on_select(select::Select *select) override; + bool on_select(select::Select *entity) override; #endif #ifdef USE_LOCK - bool on_lock(lock::Lock *a_lock) override; + bool on_lock(lock::Lock *entity) override; #endif #ifdef USE_VALVE - bool on_valve(valve::Valve *valve) override; + bool on_valve(valve::Valve *entity) override; #endif #ifdef USE_MEDIA_PLAYER - bool on_media_player(media_player::MediaPlayer *media_player) override; + bool on_media_player(media_player::MediaPlayer *entity) override; #endif #ifdef USE_ALARM_CONTROL_PANEL - bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; + bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override; #endif #ifdef USE_EVENT bool on_event(event::Event *event) override { return true; }; #endif #ifdef USE_UPDATE - bool on_update(update::UpdateEntity *update) override; + bool on_update(update::UpdateEntity *entity) override; #endif bool completed() { return this->state_ == IteratorState::NONE; } From 24bbfcdce7004b4241393b54034182a8eb6b7ab5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:42:57 -0500 Subject: [PATCH 167/293] Reduce API memory footprint through bitfield consolidation and type sizing (#9252) --- esphome/components/api/__init__.py | 7 +- esphome/components/api/api_connection.cpp | 51 ++++++----- esphome/components/api/api_connection.h | 104 ++++++++++++---------- esphome/components/api/api_server.cpp | 10 +-- esphome/components/api/api_server.h | 8 +- 5 files changed, 96 insertions(+), 84 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index bd131ef8de..501b707678 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -110,9 +110,10 @@ CONFIG_SCHEMA = cv.All( ): ACTIONS_SCHEMA, cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA, cv.Optional(CONF_ENCRYPTION): _encryption_schema, - cv.Optional( - CONF_BATCH_DELAY, default="100ms" - ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=cv.TimePeriod(milliseconds=65535)), + ), cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( single=True ), diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8550d45bfc..e5847e50f7 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -93,21 +93,21 @@ APIConnection::~APIConnection() { #ifdef HAS_PROTO_MESSAGE_DUMP void APIConnection::log_batch_item_(const DeferredBatch::BatchItem &item) { // Set log-only mode - this->log_only_mode_ = true; + this->flags_.log_only_mode = true; // Call the creator - it will create the message and log it via encode_message_to_buffer item.creator(item.entity, this, std::numeric_limits::max(), true, item.message_type); // Clear log-only mode - this->log_only_mode_ = false; + this->flags_.log_only_mode = false; } #endif void APIConnection::loop() { - if (this->next_close_) { + if (this->flags_.next_close) { // requested a disconnect this->helper_->close(); - this->remove_ = true; + this->flags_.remove = true; return; } @@ -148,15 +148,14 @@ void APIConnection::loop() { } else { this->read_message(0, buffer.type, nullptr); } - if (this->remove_) + if (this->flags_.remove) return; } } } // Process deferred batch if scheduled - if (this->deferred_batch_.batch_scheduled && - now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { + if (this->flags_.batch_scheduled && now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { this->process_batch_(); } @@ -166,7 +165,7 @@ void APIConnection::loop() { this->initial_state_iterator_.advance(); } - if (this->sent_ping_) { + if (this->flags_.sent_ping) { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { on_fatal_error(); @@ -174,13 +173,13 @@ void APIConnection::loop() { } } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) { ESP_LOGVV(TAG, "Sending keepalive PING"); - this->sent_ping_ = this->send_message(PingRequest()); - if (!this->sent_ping_) { + this->flags_.sent_ping = this->send_message(PingRequest()); + if (!this->flags_.sent_ping) { // If we can't send the ping request directly (tx_buffer full), // schedule it at the front of the batch so it will be sent with priority ESP_LOGW(TAG, "Buffer full, ping queued"); this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE); - this->sent_ping_ = true; // Mark as sent to avoid scheduling multiple pings + this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings } } @@ -240,13 +239,13 @@ DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) { // don't close yet, we still need to send the disconnect response // close will happen on next loop ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str()); - this->next_close_ = true; + this->flags_.next_close = true; DisconnectResponse resp; return resp; } void APIConnection::on_disconnect_response(const DisconnectResponse &value) { this->helper_->close(); - this->remove_ = true; + this->flags_.remove = true; } // Encodes a message to the buffer and returns the total number of bytes used, @@ -255,7 +254,7 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes uint32_t remaining_size, bool is_single) { #ifdef HAS_PROTO_MESSAGE_DUMP // If in log-only mode, just log and return - if (conn->log_only_mode_) { + if (conn->flags_.log_only_mode) { conn->log_send_message_(msg.message_name(), msg.dump()); return 1; // Return non-zero to indicate "success" for logging } @@ -1118,7 +1117,7 @@ void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { #ifdef USE_ESP32_CAMERA void APIConnection::set_camera_state(std::shared_ptr image) { - if (!this->state_subscription_) + if (!this->flags_.state_subscription) return; if (this->image_reader_.available()) return; @@ -1459,7 +1458,7 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) { #endif bool APIConnection::try_send_log_message(int level, const char *tag, const char *line) { - if (this->log_subscription_ < level) + if (this->flags_.log_subscription < level) return false; // Pre-calculate message size to avoid reallocations @@ -1500,7 +1499,7 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; resp.name = App.get_name(); - this->connection_state_ = ConnectionState::CONNECTED; + this->flags_.connection_state = static_cast(ConnectionState::CONNECTED); return resp; } ConnectResponse APIConnection::connect(const ConnectRequest &msg) { @@ -1511,7 +1510,7 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { resp.invalid_password = !correct; if (correct) { ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); - this->connection_state_ = ConnectionState::AUTHENTICATED; + this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); #ifdef USE_HOMEASSISTANT_TIME if (homeassistant::global_homeassistant_time != nullptr) { @@ -1625,7 +1624,7 @@ void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistant state_subs_at_ = 0; } bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { - if (this->remove_) + if (this->flags_.remove) return false; if (this->helper_->can_write_without_blocking()) return true; @@ -1675,7 +1674,7 @@ void APIConnection::on_no_setup_connection() { } void APIConnection::on_fatal_error() { this->helper_->close(); - this->remove_ = true; + this->flags_.remove = true; } void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type) { @@ -1700,8 +1699,8 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCre } bool APIConnection::schedule_batch_() { - if (!this->deferred_batch_.batch_scheduled) { - this->deferred_batch_.batch_scheduled = true; + if (!this->flags_.batch_scheduled) { + this->flags_.batch_scheduled = true; this->deferred_batch_.batch_start_time = App.get_loop_component_start_time(); } return true; @@ -1710,14 +1709,14 @@ bool APIConnection::schedule_batch_() { ProtoWriteBuffer APIConnection::allocate_single_message_buffer(uint16_t size) { return this->create_buffer(size); } ProtoWriteBuffer APIConnection::allocate_batch_message_buffer(uint16_t size) { - ProtoWriteBuffer result = this->prepare_message_buffer(size, this->batch_first_message_); - this->batch_first_message_ = false; + ProtoWriteBuffer result = this->prepare_message_buffer(size, this->flags_.batch_first_message); + this->flags_.batch_first_message = false; return result; } void APIConnection::process_batch_() { if (this->deferred_batch_.empty()) { - this->deferred_batch_.batch_scheduled = false; + this->flags_.batch_scheduled = false; return; } @@ -1770,7 +1769,7 @@ void APIConnection::process_batch_() { // Reserve based on estimated size (much more accurate than 24-byte worst-case) this->parent_->get_shared_buffer_ref().reserve(total_estimated_size + total_overhead); - this->batch_first_message_ = true; + this->flags_.batch_first_message = true; size_t items_processed = 0; uint16_t remaining_size = std::numeric_limits::max(); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index c9f24a7759..410a9ad3a5 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -107,7 +107,7 @@ class APIConnection : public APIServerConnection { #endif bool try_send_log_message(int level, const char *tag, const char *line); void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { - if (!this->service_call_subscription_) + if (!this->flags_.service_call_subscription) return; this->send_message(call); } @@ -164,7 +164,7 @@ class APIConnection : public APIServerConnection { void on_disconnect_response(const DisconnectResponse &value) override; void on_ping_response(const PingResponse &value) override { // we initiated ping - this->sent_ping_ = false; + this->flags_.sent_ping = false; } void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; #ifdef USE_HOMEASSISTANT_TIME @@ -177,16 +177,16 @@ class APIConnection : public APIServerConnection { DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override; void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); } void subscribe_states(const SubscribeStatesRequest &msg) override { - this->state_subscription_ = true; + this->flags_.state_subscription = true; this->initial_state_iterator_.begin(); } void subscribe_logs(const SubscribeLogsRequest &msg) override { - this->log_subscription_ = msg.level; + this->flags_.log_subscription = msg.level; if (msg.dump_config) App.schedule_dump_config(); } void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override { - this->service_call_subscription_ = true; + this->flags_.service_call_subscription = true; } void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; GetTimeResponse get_time(const GetTimeRequest &msg) override { @@ -198,9 +198,12 @@ class APIConnection : public APIServerConnection { NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override; #endif - bool is_authenticated() override { return this->connection_state_ == ConnectionState::AUTHENTICATED; } + bool is_authenticated() override { + return static_cast(this->flags_.connection_state) == ConnectionState::AUTHENTICATED; + } bool is_connection_setup() override { - return this->connection_state_ == ConnectionState ::CONNECTED || this->is_authenticated(); + return static_cast(this->flags_.connection_state) == ConnectionState::CONNECTED || + this->is_authenticated(); } void on_fatal_error() override; void on_unauthenticated_access() override; @@ -423,49 +426,28 @@ class APIConnection : public APIServerConnection { static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - // Pointers first (4 bytes each, naturally aligned) + // === Optimal member ordering for 32-bit systems === + + // Group 1: Pointers (4 bytes each on 32-bit) std::unique_ptr helper_; APIServer *parent_; - // 4-byte aligned types - uint32_t last_traffic_; - int state_subs_at_ = -1; - - // Strings (12 bytes each on 32-bit) - std::string client_info_; - std::string client_peername_; - - // 2-byte aligned types - uint16_t client_api_version_major_{0}; - uint16_t client_api_version_minor_{0}; - - // Group all 1-byte types together to minimize padding - enum class ConnectionState : uint8_t { - WAITING_FOR_HELLO, - CONNECTED, - AUTHENTICATED, - } connection_state_{ConnectionState::WAITING_FOR_HELLO}; - uint8_t log_subscription_{ESPHOME_LOG_LEVEL_NONE}; - bool remove_{false}; - bool state_subscription_{false}; - bool sent_ping_{false}; - bool service_call_subscription_{false}; - bool next_close_ = false; - // 7 bytes used, 1 byte padding -#ifdef HAS_PROTO_MESSAGE_DUMP - // When true, encode_message_to_buffer will only log, not encode - bool log_only_mode_{false}; -#endif - uint8_t ping_retries_{0}; - // 8 bytes used, no padding needed - - // Larger objects at the end + // Group 2: Larger objects (must be 4-byte aligned) + // These contain vectors/pointers internally, so putting them early ensures good alignment InitialStateIterator initial_state_iterator_; ListEntitiesIterator list_entities_iterator_; #ifdef USE_ESP32_CAMERA esp32_camera::CameraImageReader image_reader_; #endif + // Group 3: Strings (12 bytes each on 32-bit, 4-byte aligned) + std::string client_info_; + std::string client_peername_; + + // Group 4: 4-byte types + uint32_t last_traffic_; + int state_subs_at_ = -1; + // Function pointer type for message encoding using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); @@ -575,7 +557,6 @@ class APIConnection : public APIServerConnection { std::vector items; uint32_t batch_start_time{0}; - bool batch_scheduled{false}; DeferredBatch() { // Pre-allocate capacity for typical batch sizes to avoid reallocation @@ -588,13 +569,47 @@ class APIConnection : public APIServerConnection { void add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type); void clear() { items.clear(); - batch_scheduled = false; batch_start_time = 0; } bool empty() const { return items.empty(); } }; + // DeferredBatch here (16 bytes, 4-byte aligned) DeferredBatch deferred_batch_; + + // ConnectionState enum for type safety + enum class ConnectionState : uint8_t { + WAITING_FOR_HELLO = 0, + CONNECTED = 1, + AUTHENTICATED = 2, + }; + + // Group 5: Pack all small members together to minimize padding + // This group starts at a 4-byte boundary after DeferredBatch + struct APIFlags { + // Connection state only needs 2 bits (3 states) + uint8_t connection_state : 2; + // Log subscription needs 3 bits (log levels 0-7) + uint8_t log_subscription : 3; + // Boolean flags (1 bit each) + uint8_t remove : 1; + uint8_t state_subscription : 1; + uint8_t sent_ping : 1; + + uint8_t service_call_subscription : 1; + uint8_t next_close : 1; + uint8_t batch_scheduled : 1; + uint8_t batch_first_message : 1; // For batch buffer allocation +#ifdef HAS_PROTO_MESSAGE_DUMP + uint8_t log_only_mode : 1; +#endif + } flags_{}; // 2 bytes total + + // 2-byte types immediately after flags_ (no padding between them) + uint16_t client_api_version_major_{0}; + uint16_t client_api_version_minor_{0}; + // Total: 2 (flags) + 2 + 2 = 6 bytes, then 2 bytes padding to next 4-byte boundary + uint32_t get_batch_delay_ms_() const; // Message will use 8 more bytes than the minimum size, and typical // MTU is 1500. Sometimes users will see as low as 1460 MTU. @@ -612,9 +627,6 @@ class APIConnection : public APIServerConnection { bool schedule_batch_(); void process_batch_(); - // State for batch buffer allocation - bool batch_first_message_{false}; - #ifdef HAS_PROTO_MESSAGE_DUMP void log_batch_item_(const DeferredBatch::BatchItem &item); #endif diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index a33623b15a..b17faf7607 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -104,7 +104,7 @@ void APIServer::setup() { return; } for (auto &c : this->clients_) { - if (!c->remove_) + if (!c->flags_.remove) c->try_send_log_message(level, tag, message); } }); @@ -116,7 +116,7 @@ void APIServer::setup() { esp32_camera::global_esp32_camera->add_image_callback( [this](const std::shared_ptr &image) { for (auto &c : this->clients_) { - if (!c->remove_) + if (!c->flags_.remove) c->set_camera_state(image); } }); @@ -176,7 +176,7 @@ void APIServer::loop() { while (client_index < this->clients_.size()) { auto &client = this->clients_[client_index]; - if (!client->remove_) { + if (!client->flags_.remove) { // Common case: process active client client->loop(); client_index++; @@ -431,7 +431,7 @@ void APIServer::set_port(uint16_t port) { this->port_ = port; } void APIServer::set_password(const std::string &password) { this->password_ = password; } -void APIServer::set_batch_delay(uint32_t batch_delay) { this->batch_delay_ = batch_delay; } +void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; } void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) { for (auto &client : this->clients_) { @@ -502,7 +502,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { #ifdef USE_HOMEASSISTANT_TIME void APIServer::request_time() { for (auto &client : this->clients_) { - if (!client->remove_ && client->is_authenticated()) + if (!client->flags_.remove && client->is_authenticated()) client->send_time_request(); } } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 27341dc596..85c1260448 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -40,8 +40,8 @@ class APIServer : public Component, public Controller { void set_port(uint16_t port); void set_password(const std::string &password); void set_reboot_timeout(uint32_t reboot_timeout); - void set_batch_delay(uint32_t batch_delay); - uint32_t get_batch_delay() const { return batch_delay_; } + void set_batch_delay(uint16_t batch_delay); + uint16_t get_batch_delay() const { return batch_delay_; } // Get reference to shared buffer for API connections std::vector &get_shared_buffer_ref() { return shared_write_buffer_; } @@ -150,7 +150,6 @@ class APIServer : public Component, public Controller { // 4-byte aligned types uint32_t reboot_timeout_{300000}; - uint32_t batch_delay_{100}; // Vectors and strings (12 bytes each on 32-bit) std::vector> clients_; @@ -161,8 +160,9 @@ class APIServer : public Component, public Controller { // Group smaller types together uint16_t port_{6053}; + uint16_t batch_delay_{100}; bool shutting_down_ = false; - // 3 bytes used, 1 byte padding + // 5 bytes used, 3 bytes padding #ifdef USE_API_NOISE std::shared_ptr noise_ctx_ = std::make_shared(); From a4b57c7e44415e425d90c698c71093d3b81e5d78 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:43:47 -0500 Subject: [PATCH 168/293] Reduce flash usage by making add_message_object non-template (#9258) --- esphome/components/api/api_pb2_size.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_pb2_size.h b/esphome/components/api/api_pb2_size.h index e591a7350f..f371be13a5 100644 --- a/esphome/components/api/api_pb2_size.h +++ b/esphome/components/api/api_pb2_size.h @@ -316,15 +316,13 @@ class ProtoSize { /** * @brief Calculates and adds the size of a nested message field to the total message size * - * This templated version directly takes a message object, calculates its size internally, + * This version takes a ProtoMessage object, calculates its size internally, * and updates the total_size reference. This eliminates the need for a temporary variable * at the call site. * - * @tparam MessageType The type of the nested message (inferred from parameter) * @param message The nested message object */ - template - static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const MessageType &message, + static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const ProtoMessage &message, bool force = false) { uint32_t nested_size = 0; message.calculate_size(nested_size); From e907050a173011e1eb071963cbe962e907b8ec89 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:45:03 -0500 Subject: [PATCH 169/293] Remove unused return value from read_message and fix ifdef placement in generated API code (#9256) --- esphome/components/api/api_pb2_service.cpp | 161 ++++++++++----------- esphome/components/api/api_pb2_service.h | 2 +- esphome/components/api/proto.h | 2 +- script/api_protobuf/api_protobuf.py | 30 ++-- 4 files changed, 97 insertions(+), 98 deletions(-) diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 03017fdfff..de8e6574b2 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -14,7 +14,7 @@ void APIServerConnectionBase::log_send_message_(const char *name, const std::str } #endif -bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { +void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { HelloRequest msg; @@ -106,50 +106,50 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_subscribe_logs_request(msg); break; } - case 30: { #ifdef USE_COVER + case 30: { CoverCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_cover_command_request: %s", msg.dump().c_str()); #endif this->on_cover_command_request(msg); -#endif break; } - case 31: { +#endif #ifdef USE_FAN + case 31: { FanCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_fan_command_request: %s", msg.dump().c_str()); #endif this->on_fan_command_request(msg); -#endif break; } - case 32: { +#endif #ifdef USE_LIGHT + case 32: { LightCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_light_command_request: %s", msg.dump().c_str()); #endif this->on_light_command_request(msg); -#endif break; } - case 33: { +#endif #ifdef USE_SWITCH + case 33: { SwitchCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_switch_command_request: %s", msg.dump().c_str()); #endif this->on_switch_command_request(msg); -#endif break; } +#endif case 34: { SubscribeHomeassistantServicesRequest msg; msg.decode(msg_data, msg_size); @@ -204,395 +204,394 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_execute_service_request(msg); break; } - case 45: { #ifdef USE_ESP32_CAMERA + case 45: { CameraImageRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_camera_image_request: %s", msg.dump().c_str()); #endif this->on_camera_image_request(msg); -#endif break; } - case 48: { +#endif #ifdef USE_CLIMATE + case 48: { ClimateCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_climate_command_request: %s", msg.dump().c_str()); #endif this->on_climate_command_request(msg); -#endif break; } - case 51: { +#endif #ifdef USE_NUMBER + case 51: { NumberCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_number_command_request: %s", msg.dump().c_str()); #endif this->on_number_command_request(msg); -#endif break; } - case 54: { +#endif #ifdef USE_SELECT + case 54: { SelectCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str()); #endif this->on_select_command_request(msg); -#endif break; } - case 57: { +#endif #ifdef USE_SIREN + case 57: { SirenCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_siren_command_request: %s", msg.dump().c_str()); #endif this->on_siren_command_request(msg); -#endif break; } - case 60: { +#endif #ifdef USE_LOCK + case 60: { LockCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_lock_command_request: %s", msg.dump().c_str()); #endif this->on_lock_command_request(msg); -#endif break; } - case 62: { +#endif #ifdef USE_BUTTON + case 62: { ButtonCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_button_command_request: %s", msg.dump().c_str()); #endif this->on_button_command_request(msg); -#endif break; } - case 65: { +#endif #ifdef USE_MEDIA_PLAYER + case 65: { MediaPlayerCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_media_player_command_request: %s", msg.dump().c_str()); #endif this->on_media_player_command_request(msg); -#endif break; } - case 66: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 66: { SubscribeBluetoothLEAdvertisementsRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str()); #endif this->on_subscribe_bluetooth_le_advertisements_request(msg); -#endif break; } - case 68: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 68: { BluetoothDeviceRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_device_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_device_request(msg); -#endif break; } - case 70: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 70: { BluetoothGATTGetServicesRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_get_services_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_get_services_request(msg); -#endif break; } - case 73: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 73: { BluetoothGATTReadRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_read_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_read_request(msg); -#endif break; } - case 75: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 75: { BluetoothGATTWriteRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_write_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_write_request(msg); -#endif break; } - case 76: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 76: { BluetoothGATTReadDescriptorRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_read_descriptor_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_read_descriptor_request(msg); -#endif break; } - case 77: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 77: { BluetoothGATTWriteDescriptorRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_write_descriptor_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_write_descriptor_request(msg); -#endif break; } - case 78: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 78: { BluetoothGATTNotifyRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_notify_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_notify_request(msg); -#endif break; } - case 80: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 80: { SubscribeBluetoothConnectionsFreeRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_bluetooth_connections_free_request: %s", msg.dump().c_str()); #endif this->on_subscribe_bluetooth_connections_free_request(msg); -#endif break; } - case 87: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 87: { UnsubscribeBluetoothLEAdvertisementsRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_unsubscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str()); #endif this->on_unsubscribe_bluetooth_le_advertisements_request(msg); -#endif break; } - case 89: { +#endif #ifdef USE_VOICE_ASSISTANT + case 89: { SubscribeVoiceAssistantRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_voice_assistant_request: %s", msg.dump().c_str()); #endif this->on_subscribe_voice_assistant_request(msg); -#endif break; } - case 91: { +#endif #ifdef USE_VOICE_ASSISTANT + case 91: { VoiceAssistantResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_response: %s", msg.dump().c_str()); #endif this->on_voice_assistant_response(msg); -#endif break; } - case 92: { +#endif #ifdef USE_VOICE_ASSISTANT + case 92: { VoiceAssistantEventResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_event_response: %s", msg.dump().c_str()); #endif this->on_voice_assistant_event_response(msg); -#endif break; } - case 96: { +#endif #ifdef USE_ALARM_CONTROL_PANEL + case 96: { AlarmControlPanelCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_alarm_control_panel_command_request: %s", msg.dump().c_str()); #endif this->on_alarm_control_panel_command_request(msg); -#endif break; } - case 99: { +#endif #ifdef USE_TEXT + case 99: { TextCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_text_command_request: %s", msg.dump().c_str()); #endif this->on_text_command_request(msg); -#endif break; } - case 102: { +#endif #ifdef USE_DATETIME_DATE + case 102: { DateCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_date_command_request: %s", msg.dump().c_str()); #endif this->on_date_command_request(msg); -#endif break; } - case 105: { +#endif #ifdef USE_DATETIME_TIME + case 105: { TimeCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_time_command_request: %s", msg.dump().c_str()); #endif this->on_time_command_request(msg); -#endif break; } - case 106: { +#endif #ifdef USE_VOICE_ASSISTANT + case 106: { VoiceAssistantAudio msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_audio: %s", msg.dump().c_str()); #endif this->on_voice_assistant_audio(msg); -#endif break; } - case 111: { +#endif #ifdef USE_VALVE + case 111: { ValveCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_valve_command_request: %s", msg.dump().c_str()); #endif this->on_valve_command_request(msg); -#endif break; } - case 114: { +#endif #ifdef USE_DATETIME_DATETIME + case 114: { DateTimeCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_date_time_command_request: %s", msg.dump().c_str()); #endif this->on_date_time_command_request(msg); -#endif break; } - case 115: { +#endif #ifdef USE_VOICE_ASSISTANT + case 115: { VoiceAssistantTimerEventResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_timer_event_response: %s", msg.dump().c_str()); #endif this->on_voice_assistant_timer_event_response(msg); -#endif break; } - case 118: { +#endif #ifdef USE_UPDATE + case 118: { UpdateCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_update_command_request: %s", msg.dump().c_str()); #endif this->on_update_command_request(msg); -#endif break; } - case 119: { +#endif #ifdef USE_VOICE_ASSISTANT + case 119: { VoiceAssistantAnnounceRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_announce_request: %s", msg.dump().c_str()); #endif this->on_voice_assistant_announce_request(msg); -#endif break; } - case 121: { +#endif #ifdef USE_VOICE_ASSISTANT + case 121: { VoiceAssistantConfigurationRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str()); #endif this->on_voice_assistant_configuration_request(msg); -#endif break; } - case 123: { +#endif #ifdef USE_VOICE_ASSISTANT + case 123: { VoiceAssistantSetConfiguration msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_set_configuration: %s", msg.dump().c_str()); #endif this->on_voice_assistant_set_configuration(msg); -#endif break; } - case 124: { +#endif #ifdef USE_API_NOISE + case 124: { NoiseEncryptionSetKeyRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_noise_encryption_set_key_request: %s", msg.dump().c_str()); #endif this->on_noise_encryption_set_key_request(msg); -#endif break; } - case 127: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 127: { BluetoothScannerSetModeRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_scanner_set_mode_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_scanner_set_mode_request(msg); -#endif break; } +#endif default: - return false; + break; } - return true; } void APIServerConnection::on_hello_request(const HelloRequest &msg) { diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 047c56198a..3cc774f91c 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -199,7 +199,7 @@ class APIServerConnectionBase : public ProtoService { virtual void on_update_command_request(const UpdateCommandRequest &value){}; #endif protected: - bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; + void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; }; class APIServerConnection : public APIServerConnectionBase { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index d9c9e3c85d..764bac2f39 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -364,7 +364,7 @@ class ProtoService { */ virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) = 0; - virtual bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; + virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; // Optimized method that pre-allocates buffer based on message size bool send_message_(const ProtoMessage &msg, uint16_t message_type) { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 419b5aa97d..ad8e41ba5e 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1034,7 +1034,7 @@ SOURCE_BOTH = 0 SOURCE_SERVER = 1 SOURCE_CLIENT = 2 -RECEIVE_CASES: dict[int, str] = {} +RECEIVE_CASES: dict[int, tuple[str, str | None]] = {} ifdefs: dict[str, str] = {} @@ -1208,8 +1208,6 @@ def build_service_message_type( func = f"on_{snake}" hout += f"virtual void {func}(const {mt.name} &value){{}};\n" case = "" - if ifdef is not None: - case += f"#ifdef {ifdef}\n" case += f"{mt.name} msg;\n" case += "msg.decode(msg_data, msg_size);\n" if log: @@ -1217,10 +1215,9 @@ def build_service_message_type( case += f'ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' case += "#endif\n" case += f"this->{func}(msg);\n" - if ifdef is not None: - case += "#endif\n" case += "break;" - RECEIVE_CASES[id_] = case + # Store the ifdef with the case for later use + RECEIVE_CASES[id_] = (case, ifdef) # Only close ifdef if we opened it if ifdef is not None: @@ -1379,18 +1376,21 @@ def main() -> None: cases = list(RECEIVE_CASES.items()) cases.sort() hpp += " protected:\n" - hpp += " bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" - out = f"bool {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" + hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" + out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" out += " switch (msg_type) {\n" - for i, case in cases: - c = f"case {i}: {{\n" - c += indent(case) + "\n" - c += "}" - out += indent(c, " ") + "\n" + for i, (case, ifdef) in cases: + if ifdef is not None: + out += f"#ifdef {ifdef}\n" + c = f" case {i}: {{\n" + c += indent(case, " ") + "\n" + c += " }" + out += c + "\n" + if ifdef is not None: + out += "#endif\n" out += " default:\n" - out += " return false;\n" + out += " break;\n" out += " }\n" - out += " return true;\n" out += "}\n" cpp += out hpp += "};\n" From 687cb1cd2bcd2ef03f7d50ff1c16243d44def214 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:47:20 -0500 Subject: [PATCH 170/293] Reduce web_server RAM usage by 96 bytes with conditional sorting compilation (#9227) --- esphome/components/web_server/__init__.py | 2 + esphome/components/web_server/web_server.cpp | 155 ++++-------------- esphome/components/web_server/web_server.h | 6 + .../web_server_idf/web_server_idf.cpp | 2 + esphome/core/defines.h | 1 + 5 files changed, 46 insertions(+), 120 deletions(-) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index d846a3418b..8ff7ce1d16 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -211,6 +211,7 @@ async def add_entity_config(entity, config): sorting_weight = config.get(CONF_SORTING_WEIGHT, 50) sorting_group_hash = hash(config.get(CONF_SORTING_GROUP_ID)) + cg.add_define("USE_WEBSERVER_SORTING") cg.add( web_server.add_entity_config( entity, @@ -296,4 +297,5 @@ async def to_code(config): cg.add_define("USE_WEBSERVER_LOCAL") if (sorting_group_config := config.get(CONF_SORTING_GROUPS)) is not None: + cg.add_define("USE_WEBSERVER_SORTING") add_sorting_groups(var, sorting_group_config) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 927659e621..1bf3ed11cb 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -184,6 +184,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(WebServer *ws, DeferredUp std::string message = ws->get_config_json(); source->try_send_nodefer(message.c_str(), "ping", millis(), 30000); +#ifdef USE_WEBSERVER_SORTING for (auto &group : ws->sorting_groups_) { message = json::build_json([group](JsonObject root) { root["name"] = group.second.name; @@ -193,6 +194,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(WebServer *ws, DeferredUp // up to 31 groups should be able to be queued initially without defer source->try_send_nodefer(message.c_str(), "sorting_group"); } +#endif source->entities_iterator_.begin(ws->include_internal_); @@ -413,12 +415,7 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail } set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); if (!obj->get_unit_of_measurement().empty()) root["uom"] = obj->get_unit_of_measurement(); } @@ -458,12 +455,7 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std: return json::build_json([this, obj, value, start_config](JsonObject root) { set_json_icon_state_value(root, obj, "text_sensor-" + obj->get_object_id(), value, value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -511,12 +503,7 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); if (start_config == DETAIL_ALL) { root["assumed_state"] = obj->assumed_state(); - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -552,12 +539,7 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) return json::build_json([this, obj, start_config](JsonObject root) { set_json_id(root, obj, "button-" + obj->get_object_id(), start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -595,12 +577,7 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -681,12 +658,7 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { if (obj->get_traits().supports_oscillation()) root["oscillation"] = obj->oscillating; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -802,12 +774,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi for (auto const &option : obj->get_effects()) { opt.add(option->get_name()); } - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -888,12 +855,7 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { if (obj->get_traits().get_supports_tilt()) root["tilt"] = obj->tilt; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -954,12 +916,7 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail root["mode"] = (int) obj->traits.get_mode(); if (!obj->traits.get_unit_of_measurement().empty()) root["uom"] = obj->traits.get_unit_of_measurement(); - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } if (std::isnan(value)) { root["value"] = "\"NaN\""; @@ -1028,12 +985,7 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con root["value"] = value; root["state"] = value; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1091,12 +1043,7 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con root["value"] = value; root["state"] = value; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1155,12 +1102,7 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s root["value"] = value; root["state"] = value; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1221,12 +1163,7 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json root["value"] = value; if (start_config == DETAIL_ALL) { root["mode"] = (int) obj->traits.get_mode(); - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1282,12 +1219,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value for (auto &option : obj->traits.get_options()) { opt.add(option); } - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1404,12 +1336,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); } - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } bool has_state = false; @@ -1502,12 +1429,7 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet set_json_icon_state_value(root, obj, "lock-" + obj->get_object_id(), lock::lock_state_to_string(value), value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1579,12 +1501,7 @@ std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { if (obj->get_traits().get_supports_position()) root["position"] = obj->position; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1652,12 +1569,7 @@ std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmContro set_json_icon_state_value(root, obj, "alarm-control-panel-" + obj->get_object_id(), PSTR_LOCAL(alarm_control_panel_state_to_string(value)), value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1705,12 +1617,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty event_types.add(event_type); } root["device_class"] = obj->get_device_class(); - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1774,12 +1681,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c root["title"] = obj->update_info.title; root["summary"] = obj->update_info.summary; root["release_url"] = obj->update_info.release_url; - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -2089,6 +1991,18 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { bool WebServer::isRequestHandlerTrivial() const { return false; } +void WebServer::add_sorting_info_(JsonObject &root, EntityBase *entity) { +#ifdef USE_WEBSERVER_SORTING + if (this->sorting_entitys_.find(entity) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[entity].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[entity].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[entity].group_id].name; + } + } +#endif +} + +#ifdef USE_WEBSERVER_SORTING void WebServer::add_entity_config(EntityBase *entity, float weight, uint64_t group) { this->sorting_entitys_[entity] = SortingComponents{weight, group}; } @@ -2096,6 +2010,7 @@ void WebServer::add_entity_config(EntityBase *entity, float weight, uint64_t gro void WebServer::add_sorting_group(uint64_t group_id, const std::string &group_name, float weight) { this->sorting_groups_[group_id] = SortingGroup{group_name, weight}; } +#endif void WebServer::schedule_(std::function &&f) { #ifdef USE_ESP32 diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 53ee4d1212..3be99eebae 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -46,6 +46,7 @@ struct UrlMatch { bool valid; ///< Whether this match is valid }; +#ifdef USE_WEBSERVER_SORTING struct SortingComponents { float weight; uint64_t group_id; @@ -55,6 +56,7 @@ struct SortingGroup { std::string name; float weight; }; +#endif enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; @@ -474,14 +476,18 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// This web handle is not trivial. bool isRequestHandlerTrivial() const override; // NOLINT(readability-identifier-naming) +#ifdef USE_WEBSERVER_SORTING void add_entity_config(EntityBase *entity, float weight, uint64_t group); void add_sorting_group(uint64_t group_id, const std::string &group_name, float weight); std::map sorting_entitys_; std::map sorting_groups_; +#endif + bool include_internal_{false}; protected: + void add_sorting_info_(JsonObject &root, EntityBase *entity); void schedule_(std::function &&f); web_server_base::WebServerBase *base_; #ifdef USE_ARDUINO diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 90fdf720cd..30c6b04fb2 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -338,6 +338,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * std::string message = ws->get_config_json(); this->try_send_nodefer(message.c_str(), "ping", millis(), 30000); +#ifdef USE_WEBSERVER_SORTING for (auto &group : ws->sorting_groups_) { message = json::build_json([group](JsonObject root) { root["name"] = group.second.name; @@ -348,6 +349,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * // since the only thing in the send buffer at this point is the initial ping/config this->try_send_nodefer(message.c_str(), "sorting_group"); } +#endif this->entities_iterator_->begin(ws->include_internal_); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 8abd6598f7..22454249aa 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -151,6 +151,7 @@ #define USE_VOICE_ASSISTANT #define USE_WEBSERVER #define USE_WEBSERVER_PORT 80 // NOLINT +#define USE_WEBSERVER_SORTING #define USE_WIFI_11KV_SUPPORT #ifdef USE_ARDUINO From 2289073a1e0b8013cc6b840ea47cb0002078d94f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:47:50 -0500 Subject: [PATCH 171/293] Add interrupt support to GPIO binary sensors (#9115) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../components/gpio/binary_sensor/__init__.py | 17 ++++ .../gpio/binary_sensor/gpio_binary_sensor.cpp | 80 ++++++++++++++++++- .../gpio/binary_sensor/gpio_binary_sensor.h | 40 ++++++++++ 3 files changed, 134 insertions(+), 3 deletions(-) diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 23f2781095..9f50fd779a 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -10,11 +10,24 @@ GPIOBinarySensor = gpio_ns.class_( "GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component ) +CONF_USE_INTERRUPT = "use_interrupt" +CONF_INTERRUPT_TYPE = "interrupt_type" + +INTERRUPT_TYPES = { + "RISING": gpio_ns.INTERRUPT_RISING_EDGE, + "FALLING": gpio_ns.INTERRUPT_FALLING_EDGE, + "ANY": gpio_ns.INTERRUPT_ANY_EDGE, +} + CONFIG_SCHEMA = ( binary_sensor.binary_sensor_schema(GPIOBinarySensor) .extend( { cv.Required(CONF_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_USE_INTERRUPT, default=True): cv.boolean, + cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum( + INTERRUPT_TYPES, upper=True + ), } ) .extend(cv.COMPONENT_SCHEMA) @@ -27,3 +40,7 @@ async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) + + cg.add(var.set_use_interrupt(config[CONF_USE_INTERRUPT])) + if config[CONF_USE_INTERRUPT]: + cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index cf4b088580..4b8369cd59 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -6,17 +6,91 @@ namespace gpio { static const char *const TAG = "gpio.binary_sensor"; +void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) { + bool new_state = arg->isr_pin_.digital_read(); + if (new_state != arg->last_state_) { + arg->state_ = new_state; + arg->last_state_ = new_state; + arg->changed_ = true; + // Wake up the component from its disabled loop state + if (arg->component_ != nullptr) { + arg->component_->enable_loop_soon_any_context(); + } + } +} + +void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component) { + pin->setup(); + this->isr_pin_ = pin->to_isr(); + this->component_ = component; + + // Read initial state + this->last_state_ = pin->digital_read(); + this->state_ = this->last_state_; + + // Attach interrupt - from this point on, any changes will be caught by the interrupt + pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, type); +} + void GPIOBinarySensor::setup() { - this->pin_->setup(); - this->publish_initial_state(this->pin_->digital_read()); + if (this->use_interrupt_ && !this->pin_->is_internal()) { + ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode"); + this->use_interrupt_ = false; + } + + if (this->use_interrupt_) { + auto *internal_pin = static_cast(this->pin_); + this->store_.setup(internal_pin, this->interrupt_type_, this); + this->publish_initial_state(this->store_.get_state()); + } else { + this->pin_->setup(); + this->publish_initial_state(this->pin_->digital_read()); + } } void GPIOBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this); LOG_PIN(" Pin: ", this->pin_); + const char *mode = this->use_interrupt_ ? "interrupt" : "polling"; + ESP_LOGCONFIG(TAG, " Mode: %s", mode); + if (this->use_interrupt_) { + const char *interrupt_type; + switch (this->interrupt_type_) { + case gpio::INTERRUPT_RISING_EDGE: + interrupt_type = "RISING_EDGE"; + break; + case gpio::INTERRUPT_FALLING_EDGE: + interrupt_type = "FALLING_EDGE"; + break; + case gpio::INTERRUPT_ANY_EDGE: + interrupt_type = "ANY_EDGE"; + break; + default: + interrupt_type = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " Interrupt Type: %s", interrupt_type); + } } -void GPIOBinarySensor::loop() { this->publish_state(this->pin_->digital_read()); } +void GPIOBinarySensor::loop() { + if (this->use_interrupt_) { + if (this->store_.is_changed()) { + // Clear the flag immediately to minimize the window where we might miss changes + this->store_.clear_changed(); + // Read the state and publish it + // Note: If the ISR fires between clear_changed() and get_state(), that's fine - + // we'll process the new change on the next loop iteration + bool state = this->store_.get_state(); + this->publish_state(state); + } else { + // No changes, disable the loop until the next interrupt + this->disable_loop(); + } + } else { + this->publish_state(this->pin_->digital_read()); + } +} float GPIOBinarySensor::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h index 33a173fe2e..8cf52f540b 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h @@ -2,14 +2,51 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/components/binary_sensor/binary_sensor.h" namespace esphome { namespace gpio { +// Store class for ISR data (no vtables, ISR-safe) +class GPIOBinarySensorStore { + public: + void setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component); + + static void gpio_intr(GPIOBinarySensorStore *arg); + + bool get_state() const { + // No lock needed: state_ is atomically updated by ISR + // Volatile ensures we read the latest value + return this->state_; + } + + bool is_changed() const { + // Simple read of volatile bool - no clearing here + return this->changed_; + } + + void clear_changed() { + // Separate method to clear the flag + this->changed_ = false; + } + + protected: + ISRInternalGPIOPin isr_pin_; + volatile bool state_{false}; + volatile bool last_state_{false}; + volatile bool changed_{false}; + Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context() +}; + class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component { public: + // No destructor needed: ESPHome components are created at boot and live forever. + // Interrupts are only detached on reboot when memory is cleared anyway. + void set_pin(GPIOPin *pin) { pin_ = pin; } + void set_use_interrupt(bool use_interrupt) { use_interrupt_ = use_interrupt; } + void set_interrupt_type(gpio::InterruptType type) { interrupt_type_ = type; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup pin @@ -22,6 +59,9 @@ class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component { protected: GPIOPin *pin_; + bool use_interrupt_{true}; + gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE}; + GPIOBinarySensorStore store_; }; } // namespace gpio From 53e9ffe656c920218ff858c3072c9f1df962a562 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 30 Jun 2025 11:48:19 +1200 Subject: [PATCH 172/293] [pi4ioe5v6408] Add new IO Expander (#8888) Co-authored-by: Keith Burzinski --- CODEOWNERS | 1 + esphome/components/pi4ioe5v6408/__init__.py | 84 +++++++++ .../components/pi4ioe5v6408/pi4ioe5v6408.cpp | 171 ++++++++++++++++++ .../components/pi4ioe5v6408/pi4ioe5v6408.h | 70 +++++++ tests/components/pi4ioe5v6408/common.yaml | 22 +++ .../pi4ioe5v6408/test.esp32-ard.yaml | 5 + .../pi4ioe5v6408/test.esp32-idf.yaml | 5 + .../pi4ioe5v6408/test.rp2040-ard.yaml | 5 + 8 files changed, 363 insertions(+) create mode 100644 esphome/components/pi4ioe5v6408/__init__.py create mode 100644 esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp create mode 100644 esphome/components/pi4ioe5v6408/pi4ioe5v6408.h create mode 100644 tests/components/pi4ioe5v6408/common.yaml create mode 100644 tests/components/pi4ioe5v6408/test.esp32-ard.yaml create mode 100644 tests/components/pi4ioe5v6408/test.esp32-idf.yaml create mode 100644 tests/components/pi4ioe5v6408/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 832c571ae4..b3c66c775b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -332,6 +332,7 @@ esphome/components/pca6416a/* @Mat931 esphome/components/pca9554/* @clydebarrow @hwstar esphome/components/pcf85063/* @brogon esphome/components/pcf8563/* @KoenBreeman +esphome/components/pi4ioe5v6408/* @jesserockz esphome/components/pid/* @OttoWinter esphome/components/pipsolar/* @andreashergert1984 esphome/components/pm1006/* @habbie diff --git a/esphome/components/pi4ioe5v6408/__init__.py b/esphome/components/pi4ioe5v6408/__init__.py new file mode 100644 index 0000000000..c64f923823 --- /dev/null +++ b/esphome/components/pi4ioe5v6408/__init__.py @@ -0,0 +1,84 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OUTPUT, + CONF_PULLDOWN, + CONF_PULLUP, + CONF_RESET, +) + +AUTO_LOAD = ["gpio_expander"] +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + + +pi4ioe5v6408_ns = cg.esphome_ns.namespace("pi4ioe5v6408") +PI4IOE5V6408Component = pi4ioe5v6408_ns.class_( + "PI4IOE5V6408Component", cg.Component, i2c.I2CDevice +) +PI4IOE5V6408GPIOPin = pi4ioe5v6408_ns.class_("PI4IOE5V6408GPIOPin", cg.GPIOPin) + +CONF_PI4IOE5V6408 = "pi4ioe5v6408" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(PI4IOE5V6408Component), + cv.Optional(CONF_RESET, default=True): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x43)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + cg.add(var.set_reset(config[CONF_RESET])) + + +def validate_mode(value): + if not (value[CONF_INPUT] or value[CONF_OUTPUT]): + raise cv.Invalid("Mode must be either input or output") + if value[CONF_INPUT] and value[CONF_OUTPUT]: + raise cv.Invalid("Mode must be either input or output") + return value + + +PI4IOE5V6408_PIN_SCHEMA = pins.gpio_base_schema( + PI4IOE5V6408GPIOPin, + cv.int_range(min=0, max=7), + modes=[ + CONF_INPUT, + CONF_OUTPUT, + CONF_PULLUP, + CONF_PULLDOWN, + ], + mode_validator=validate_mode, +).extend( + { + cv.Required(CONF_PI4IOE5V6408): cv.use_id(PI4IOE5V6408Component), + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_PI4IOE5V6408, PI4IOE5V6408_PIN_SCHEMA) +async def pi4ioe5v6408_pin_schema(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(var, config[CONF_PI4IOE5V6408]) + + cg.add(var.set_pin(config[CONF_NUMBER])) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp new file mode 100644 index 0000000000..55b8edffc8 --- /dev/null +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -0,0 +1,171 @@ +#include "pi4ioe5v6408.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pi4ioe5v6408 { + +static const uint8_t PI4IOE5V6408_REGISTER_DEVICE_ID = 0x01; +static const uint8_t PI4IOE5V6408_REGISTER_IO_DIR = 0x03; +static const uint8_t PI4IOE5V6408_REGISTER_OUT_SET = 0x05; +static const uint8_t PI4IOE5V6408_REGISTER_OUT_HIGH_IMPEDENCE = 0x07; +static const uint8_t PI4IOE5V6408_REGISTER_IN_DEFAULT_STATE = 0x09; +static const uint8_t PI4IOE5V6408_REGISTER_PULL_ENABLE = 0x0B; +static const uint8_t PI4IOE5V6408_REGISTER_PULL_SELECT = 0x0D; +static const uint8_t PI4IOE5V6408_REGISTER_IN_STATE = 0x0F; +static const uint8_t PI4IOE5V6408_REGISTER_INTERRUPT_ENABLE_MASK = 0x11; +static const uint8_t PI4IOE5V6408_REGISTER_INTERRUPT_STATUS = 0x13; + +static const char *const TAG = "pi4ioe5v6408"; + +void PI4IOE5V6408Component::setup() { + ESP_LOGCONFIG(TAG, "Running setup"); + if (this->reset_) { + this->reg(PI4IOE5V6408_REGISTER_DEVICE_ID) |= 0b00000001; + this->reg(PI4IOE5V6408_REGISTER_OUT_HIGH_IMPEDENCE) = 0b00000000; + } else { + if (!this->read_gpio_modes_()) { + this->mark_failed(); + ESP_LOGE(TAG, "Failed to read GPIO modes"); + return; + } + if (!this->read_gpio_outputs_()) { + this->mark_failed(); + ESP_LOGE(TAG, "Failed to read GPIO outputs"); + return; + } + } +} +void PI4IOE5V6408Component::dump_config() { + ESP_LOGCONFIG(TAG, "PI4IOE5V6408:"); + LOG_I2C_DEVICE(this) + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} +void PI4IOE5V6408Component::pin_mode(uint8_t pin, gpio::Flags flags) { + if (flags & gpio::FLAG_OUTPUT) { + // Set mode mask bit + this->mode_mask_ |= 1 << pin; + } else if (flags & gpio::FLAG_INPUT) { + // Clear mode mask bit + this->mode_mask_ &= ~(1 << pin); + if (flags & gpio::FLAG_PULLUP) { + this->pull_up_down_mask_ |= 1 << pin; + this->pull_enable_mask_ |= 1 << pin; + } else if (flags & gpio::FLAG_PULLDOWN) { + this->pull_up_down_mask_ &= ~(1 << pin); + this->pull_enable_mask_ |= 1 << pin; + } + } + // Write GPIO to enable input mode + this->write_gpio_modes_(); +} + +void PI4IOE5V6408Component::loop() { this->reset_pin_cache_(); } + +bool PI4IOE5V6408Component::read_gpio_outputs_() { + if (this->is_failed()) + return false; + + uint8_t data; + if (!this->read_byte(PI4IOE5V6408_REGISTER_OUT_SET, &data)) { + this->status_set_warning("Failed to read output register"); + return false; + } + this->output_mask_ = data; + this->status_clear_warning(); + return true; +} + +bool PI4IOE5V6408Component::read_gpio_modes_() { + if (this->is_failed()) + return false; + + uint8_t data; + if (!this->read_byte(PI4IOE5V6408_REGISTER_IO_DIR, &data)) { + this->status_set_warning("Failed to read GPIO modes"); + return false; + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, "Read GPIO modes: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(data)); +#endif + this->mode_mask_ = data; + this->status_clear_warning(); + return true; +} + +bool PI4IOE5V6408Component::digital_read_hw(uint8_t pin) { + if (this->is_failed()) + return false; + + uint8_t data; + if (!this->read_byte(PI4IOE5V6408_REGISTER_IN_STATE, &data)) { + this->status_set_warning("Failed to read GPIO state"); + return false; + } + this->input_mask_ = data; + this->status_clear_warning(); + return true; +} + +void PI4IOE5V6408Component::digital_write_hw(uint8_t pin, bool value) { + if (this->is_failed()) + return; + + if (value) { + this->output_mask_ |= (1 << pin); + } else { + this->output_mask_ &= ~(1 << pin); + } + if (!this->write_byte(PI4IOE5V6408_REGISTER_OUT_SET, this->output_mask_)) { + this->status_set_warning("Failed to write output register"); + return; + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, "Wrote GPIO output: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(this->output_mask_)); +#endif + this->status_clear_warning(); +} + +bool PI4IOE5V6408Component::write_gpio_modes_() { + if (this->is_failed()) + return false; + + if (!this->write_byte(PI4IOE5V6408_REGISTER_IO_DIR, this->mode_mask_)) { + this->status_set_warning("Failed to write GPIO modes"); + return false; + } + if (!this->write_byte(PI4IOE5V6408_REGISTER_PULL_SELECT, this->pull_up_down_mask_)) { + this->status_set_warning("Failed to write GPIO pullup/pulldown"); + return false; + } + if (!this->write_byte(PI4IOE5V6408_REGISTER_PULL_ENABLE, this->pull_enable_mask_)) { + this->status_set_warning("Failed to write GPIO pull enable"); + return false; + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, + "Wrote GPIO modes: 0b" BYTE_TO_BINARY_PATTERN "\n" + "Wrote GPIO pullup/pulldown: 0b" BYTE_TO_BINARY_PATTERN "\n" + "Wrote GPIO pull enable: 0b" BYTE_TO_BINARY_PATTERN, + BYTE_TO_BINARY(this->mode_mask_), BYTE_TO_BINARY(this->pull_up_down_mask_), + BYTE_TO_BINARY(this->pull_enable_mask_)); +#endif + this->status_clear_warning(); + return true; +} + +bool PI4IOE5V6408Component::digital_read_cache(uint8_t pin) { return (this->input_mask_ & (1 << pin)); } + +float PI4IOE5V6408Component::get_setup_priority() const { return setup_priority::IO; } + +void PI4IOE5V6408GPIOPin::setup() { this->pin_mode(this->flags_); } +void PI4IOE5V6408GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +bool PI4IOE5V6408GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void PI4IOE5V6408GPIOPin::digital_write(bool value) { + this->parent_->digital_write(this->pin_, value != this->inverted_); +} +std::string PI4IOE5V6408GPIOPin::dump_summary() const { return str_sprintf("%u via PI4IOE5V6408", this->pin_); } + +} // namespace pi4ioe5v6408 +} // namespace esphome diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h new file mode 100644 index 0000000000..82b3076fab --- /dev/null +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h @@ -0,0 +1,70 @@ +#pragma once + +#include "esphome/components/gpio_expander/cached_gpio.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace pi4ioe5v6408 { +class PI4IOE5V6408Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { + public: + PI4IOE5V6408Component() = default; + + void setup() override; + void pin_mode(uint8_t pin, gpio::Flags flags); + + float get_setup_priority() const override; + void dump_config() override; + void loop() override; + + /// Indicate if the component should reset the state during setup + void set_reset(bool reset) { this->reset_ = reset; } + + protected: + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + + /// Mask for the pin mode - 1 means output, 0 means input + uint8_t mode_mask_{0x00}; + /// The mask to write as output state - 1 means HIGH, 0 means LOW + uint8_t output_mask_{0x00}; + /// The state read in digital_read_hw - 1 means HIGH, 0 means LOW + uint8_t input_mask_{0x00}; + /// The mask to write as input buffer state - 1 means enabled, 0 means disabled + uint8_t pull_enable_mask_{0x00}; + /// The mask to write as pullup state - 1 means pullup, 0 means pulldown + uint8_t pull_up_down_mask_{0x00}; + + bool reset_{true}; + + bool read_gpio_modes_(); + bool write_gpio_modes_(); + bool read_gpio_outputs_(); +}; + +class PI4IOE5V6408GPIOPin : public GPIOPin, public Parented { + public: + void setup() override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + + void set_pin(uint8_t pin) { this->pin_ = pin; } + void set_inverted(bool inverted) { this->inverted_ = inverted; } + void set_flags(gpio::Flags flags) { this->flags_ = flags; } + + gpio::Flags get_flags() const override { return this->flags_; } + + protected: + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace pi4ioe5v6408 +} // namespace esphome diff --git a/tests/components/pi4ioe5v6408/common.yaml b/tests/components/pi4ioe5v6408/common.yaml new file mode 100644 index 0000000000..4130dc2652 --- /dev/null +++ b/tests/components/pi4ioe5v6408/common.yaml @@ -0,0 +1,22 @@ +i2c: + id: i2c_pi4ioe5v6408 + sda: ${i2c_sda} + scl: ${i2c_scl} + +pi4ioe5v6408: + id: pi4ioe1 + address: 0x44 + +switch: + - platform: gpio + id: switch1 + pin: + pi4ioe5v6408: pi4ioe1 + number: 0 + +binary_sensor: + - platform: gpio + id: sensor1 + pin: + pi4ioe5v6408: pi4ioe1 + number: 1 diff --git a/tests/components/pi4ioe5v6408/test.esp32-ard.yaml b/tests/components/pi4ioe5v6408/test.esp32-ard.yaml new file mode 100644 index 0000000000..55e6edfbf3 --- /dev/null +++ b/tests/components/pi4ioe5v6408/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_sda: GPIO21 + i2c_scl: GPIO22 + +<<: !include common.yaml diff --git a/tests/components/pi4ioe5v6408/test.esp32-idf.yaml b/tests/components/pi4ioe5v6408/test.esp32-idf.yaml new file mode 100644 index 0000000000..55e6edfbf3 --- /dev/null +++ b/tests/components/pi4ioe5v6408/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_sda: GPIO21 + i2c_scl: GPIO22 + +<<: !include common.yaml diff --git a/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml b/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b7b6b13bfe --- /dev/null +++ b/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_sda: GPIO4 + i2c_scl: GPIO5 + +<<: !include common.yaml From 0b1b8f05e11e2ec11218f80384b20edabad022db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:49:31 -0500 Subject: [PATCH 173/293] Reduce loop enable/disable log spam by using very verbose level (#9267) --- esphome/core/application.cpp | 2 +- esphome/core/component.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 328de00640..1599c648e7 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -376,7 +376,7 @@ void Application::enable_pending_loops_() { // Clear the pending flag and enable the loop component->pending_enable_loop_ = false; - ESP_LOGD(TAG, "%s loop enabled from ISR", component->get_component_source()); + ESP_LOGVV(TAG, "%s loop enabled from ISR", component->get_component_source()); component->component_state_ &= ~COMPONENT_STATE_MASK; component->component_state_ |= COMPONENT_STATE_LOOP; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 625a7b2125..8fa63de84e 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -149,7 +149,7 @@ void Component::mark_failed() { } void Component::disable_loop() { if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { - ESP_LOGD(TAG, "%s loop disabled", this->get_component_source()); + ESP_LOGVV(TAG, "%s loop disabled", this->get_component_source()); this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ |= COMPONENT_STATE_LOOP_DONE; App.disable_component_loop_(this); @@ -157,7 +157,7 @@ void Component::disable_loop() { } void Component::enable_loop() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { - ESP_LOGD(TAG, "%s loop enabled", this->get_component_source()); + ESP_LOGVV(TAG, "%s loop enabled", this->get_component_source()); this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ |= COMPONENT_STATE_LOOP; App.enable_component_loop_(this); From 7f8dd4b2540b406e51150e750d8d92abf4843435 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 19:19:18 -0500 Subject: [PATCH 174/293] Fix thread-safe cleanup of event source connections in ESP-IDF web server (#9268) --- .../web_server_idf/web_server_idf.cpp | 39 ++++++++++++++----- .../web_server_idf/web_server_idf.h | 3 +- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 30c6b04fb2..409230806c 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -292,21 +292,38 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) { } void AsyncEventSource::loop() { - for (auto *ses : this->sessions_) { - ses->loop(); + // Clean up dead sessions safely + // This follows the ESP-IDF pattern where free_ctx marks resources as dead + // and the main loop handles the actual cleanup to avoid race conditions + auto it = this->sessions_.begin(); + while (it != this->sessions_.end()) { + auto *ses = *it; + // If the session has a dead socket (marked by destroy callback) + if (ses->fd_.load() == 0) { + ESP_LOGD(TAG, "Removing dead event source session"); + it = this->sessions_.erase(it); + delete ses; // NOLINT(cppcoreguidelines-owning-memory) + } else { + ses->loop(); + ++it; + } } } void AsyncEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) { for (auto *ses : this->sessions_) { - ses->try_send_nodefer(message, event, id, reconnect); + if (ses->fd_.load() != 0) { // Skip dead sessions + ses->try_send_nodefer(message, event, id, reconnect); + } } } void AsyncEventSource::deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator) { for (auto *ses : this->sessions_) { - ses->deferrable_send_state(source, event_type, message_generator); + if (ses->fd_.load() != 0) { // Skip dead sessions + ses->deferrable_send_state(source, event_type, message_generator); + } } } @@ -331,7 +348,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * req->free_ctx = AsyncEventSourceResponse::destroy; this->hd_ = req->handle; - this->fd_ = httpd_req_to_sockfd(req); + this->fd_.store(httpd_req_to_sockfd(req)); // Configure reconnect timeout and send config // this should always go through since the tcp send buffer is empty on connect @@ -362,8 +379,10 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * void AsyncEventSourceResponse::destroy(void *ptr) { auto *rsp = static_cast(ptr); - rsp->server_->sessions_.erase(rsp); - delete rsp; // NOLINT(cppcoreguidelines-owning-memory) + ESP_LOGD(TAG, "Event source connection closed (fd: %d)", rsp->fd_.load()); + // Mark as dead by setting fd to 0 - will be cleaned up in the main loop + rsp->fd_.store(0); + // Note: We don't delete or remove from set here to avoid race conditions } // helper for allowing only unique entries in the queue @@ -403,9 +422,11 @@ void AsyncEventSourceResponse::process_buffer_() { return; } - int bytes_sent = httpd_socket_send(this->hd_, this->fd_, event_buffer_.c_str() + event_bytes_sent_, + int bytes_sent = httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, event_buffer_.size() - event_bytes_sent_, 0); if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT || bytes_sent == HTTPD_SOCK_ERR_FAIL) { + // Socket error - just return, the connection will be closed by httpd + // and our destroy callback will be called return; } event_bytes_sent_ += bytes_sent; @@ -425,7 +446,7 @@ void AsyncEventSourceResponse::loop() { bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) { - if (this->fd_ == 0) { + if (this->fd_.load() == 0) { return false; } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 8dafdf11ef..7547117224 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -4,6 +4,7 @@ #include "esphome/core/defines.h" #include +#include #include #include #include @@ -271,7 +272,7 @@ class AsyncEventSourceResponse { static void destroy(void *p); AsyncEventSource *server_; httpd_handle_t hd_{}; - int fd_{}; + std::atomic fd_{}; std::vector deferred_queue_; esphome::web_server::WebServer *web_server_; std::unique_ptr entities_iterator_; From 6a354d7c946d5f81e0efd355e38d4a4267f39152 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 22:33:35 -0500 Subject: [PATCH 175/293] Reduce API component memory usage with conditional compilation (#9262) --- esphome/components/api/__init__.py | 34 +-- esphome/components/api/api_connection.cpp | 2 + esphome/components/api/api_server.cpp | 2 + esphome/components/api/api_server.h | 38 +++- esphome/core/defines.h | 3 + .../fixtures/api_conditional_memory.yaml | 71 ++++++ .../test_api_conditional_memory.py | 205 ++++++++++++++++++ 7 files changed, 338 insertions(+), 17 deletions(-) create mode 100644 tests/integration/fixtures/api_conditional_memory.yaml create mode 100644 tests/integration/test_api_conditional_memory.py diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 501b707678..ae83129c21 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -136,23 +136,26 @@ async def to_code(config): cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY])) - for conf in config.get(CONF_ACTIONS, []): - template_args = [] - func_args = [] - service_arg_names = [] - for name, var_ in conf[CONF_VARIABLES].items(): - native = SERVICE_ARG_NATIVE_TYPES[var_] - template_args.append(native) - func_args.append((native, name)) - service_arg_names.append(name) - templ = cg.TemplateArguments(*template_args) - trigger = cg.new_Pvariable( - conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names - ) - cg.add(var.register_user_service(trigger)) - await automation.build_automation(trigger, func_args, conf) + if actions := config.get(CONF_ACTIONS, []): + cg.add_define("USE_API_YAML_SERVICES") + for conf in actions: + template_args = [] + func_args = [] + service_arg_names = [] + for name, var_ in conf[CONF_VARIABLES].items(): + native = SERVICE_ARG_NATIVE_TYPES[var_] + template_args.append(native) + func_args.append((native, name)) + service_arg_names.append(name) + templ = cg.TemplateArguments(*template_args) + trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names + ) + cg.add(var.register_user_service(trigger)) + await automation.build_automation(trigger, func_args, conf) if CONF_ON_CLIENT_CONNECTED in config: + cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER") await automation.build_automation( var.get_client_connected_trigger(), [(cg.std_string, "client_info"), (cg.std_string, "client_address")], @@ -160,6 +163,7 @@ async def to_code(config): ) if CONF_ON_CLIENT_DISCONNECTED in config: + cg.add_define("USE_API_CLIENT_DISCONNECTED_TRIGGER") await automation.build_automation( var.get_client_disconnected_trigger(), [(cg.std_string, "client_info"), (cg.std_string, "client_address")], diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index e5847e50f7..6a40f21f99 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1511,7 +1511,9 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { if (correct) { ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); +#ifdef USE_API_CLIENT_CONNECTED_TRIGGER this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); +#endif #ifdef USE_HOMEASSISTANT_TIME if (homeassistant::global_homeassistant_time != nullptr) { this->send_time_request(); diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index b17faf7607..ebe80604dc 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -184,7 +184,9 @@ void APIServer::loop() { } // Rare case: handle disconnection +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_); +#endif ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str()); // Swap with the last element and pop (avoids expensive vector shifts) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 85c1260448..5a9b0677bc 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -105,7 +105,18 @@ class APIServer : public Component, public Controller { void on_media_player_update(media_player::MediaPlayer *obj) override; #endif void send_homeassistant_service_call(const HomeassistantServiceResponse &call); - void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } + void register_user_service(UserServiceDescriptor *descriptor) { +#ifdef USE_API_YAML_SERVICES + // Vector is pre-allocated when services are defined in YAML + this->user_services_.push_back(descriptor); +#else + // Lazy allocate vector on first use for CustomAPIDevice + if (!this->user_services_) { + this->user_services_ = std::make_unique>(); + } + this->user_services_->push_back(descriptor); +#endif + } #ifdef USE_HOMEASSISTANT_TIME void request_time(); #endif @@ -134,19 +145,34 @@ class APIServer : public Component, public Controller { void get_home_assistant_state(std::string entity_id, optional attribute, std::function f); const std::vector &get_state_subs() const; - const std::vector &get_user_services() const { return this->user_services_; } + const std::vector &get_user_services() const { +#ifdef USE_API_YAML_SERVICES + return this->user_services_; +#else + static const std::vector EMPTY; + return this->user_services_ ? *this->user_services_ : EMPTY; +#endif + } +#ifdef USE_API_CLIENT_CONNECTED_TRIGGER Trigger *get_client_connected_trigger() const { return this->client_connected_trigger_; } +#endif +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER Trigger *get_client_disconnected_trigger() const { return this->client_disconnected_trigger_; } +#endif protected: void schedule_reboot_timeout_(); // Pointers and pointer-like types first (4 bytes each) std::unique_ptr socket_ = nullptr; +#ifdef USE_API_CLIENT_CONNECTED_TRIGGER Trigger *client_connected_trigger_ = new Trigger(); +#endif +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER Trigger *client_disconnected_trigger_ = new Trigger(); +#endif // 4-byte aligned types uint32_t reboot_timeout_{300000}; @@ -156,7 +182,15 @@ class APIServer : public Component, public Controller { std::string password_; std::vector shared_write_buffer_; // Shared proto write buffer for all connections std::vector state_subs_; +#ifdef USE_API_YAML_SERVICES + // When services are defined in YAML, we know at compile time that services will be registered std::vector user_services_; +#else + // Services can still be registered at runtime by CustomAPIDevice components even when not + // defined in YAML. Using unique_ptr allows lazy allocation, saving 12 bytes in the common + // case where no services (YAML or custom) are used. + std::unique_ptr> user_services_; +#endif // Group smaller types together uint16_t port_{6053}; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 22454249aa..ea3c8bdc17 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -101,8 +101,11 @@ #define USE_AUDIO_FLAC_SUPPORT #define USE_AUDIO_MP3_SUPPORT #define USE_API +#define USE_API_CLIENT_CONNECTED_TRIGGER +#define USE_API_CLIENT_DISCONNECTED_TRIGGER #define USE_API_NOISE #define USE_API_PLAINTEXT +#define USE_API_YAML_SERVICES #define USE_MD5 #define USE_MQTT #define USE_NETWORK diff --git a/tests/integration/fixtures/api_conditional_memory.yaml b/tests/integration/fixtures/api_conditional_memory.yaml new file mode 100644 index 0000000000..4bbba5084b --- /dev/null +++ b/tests/integration/fixtures/api_conditional_memory.yaml @@ -0,0 +1,71 @@ +esphome: + name: api-conditional-memory-test +host: +api: + actions: + - action: test_simple_service + then: + - logger.log: "Simple service called" + - binary_sensor.template.publish: + id: service_called_sensor + state: ON + - action: test_service_with_args + variables: + arg_string: string + arg_int: int + arg_bool: bool + arg_float: float + then: + - logger.log: + format: "Service called with: %s, %d, %d, %.2f" + args: [arg_string.c_str(), arg_int, arg_bool, arg_float] + - sensor.template.publish: + id: service_arg_sensor + state: !lambda 'return arg_float;' + on_client_connected: + - logger.log: + format: "Client %s connected from %s" + args: [client_info.c_str(), client_address.c_str()] + - binary_sensor.template.publish: + id: client_connected + state: ON + - text_sensor.template.publish: + id: last_client_info + state: !lambda 'return client_info;' + on_client_disconnected: + - logger.log: + format: "Client %s disconnected from %s" + args: [client_info.c_str(), client_address.c_str()] + - binary_sensor.template.publish: + id: client_connected + state: OFF + - binary_sensor.template.publish: + id: client_disconnected_event + state: ON + +logger: + level: DEBUG + +binary_sensor: + - platform: template + name: "Client Connected" + id: client_connected + device_class: connectivity + - platform: template + name: "Client Disconnected Event" + id: client_disconnected_event + - platform: template + name: "Service Called" + id: service_called_sensor + +sensor: + - platform: template + name: "Service Argument Value" + id: service_arg_sensor + unit_of_measurement: "" + accuracy_decimals: 2 + +text_sensor: + - platform: template + name: "Last Client Info" + id: last_client_info diff --git a/tests/integration/test_api_conditional_memory.py b/tests/integration/test_api_conditional_memory.py new file mode 100644 index 0000000000..b85e8d91af --- /dev/null +++ b/tests/integration/test_api_conditional_memory.py @@ -0,0 +1,205 @@ +"""Integration test for API conditional memory optimization with triggers and services.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import ( + BinarySensorInfo, + EntityState, + SensorInfo, + TextSensorInfo, + UserService, + UserServiceArgType, +) +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_api_conditional_memory( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test API triggers and services work correctly with conditional compilation.""" + loop = asyncio.get_running_loop() + # Keep ESPHome process running throughout the test + async with run_compiled(yaml_config): + # First connection + async with api_client_connected() as client: + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "api-conditional-memory-test" + + # List entities and services + entity_info, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our entities + client_connected: BinarySensorInfo | None = None + client_disconnected_event: BinarySensorInfo | None = None + service_called_sensor: BinarySensorInfo | None = None + service_arg_sensor: SensorInfo | None = None + last_client_info: TextSensorInfo | None = None + + for entity in entity_info: + if isinstance(entity, BinarySensorInfo): + if entity.object_id == "client_connected": + client_connected = entity + elif entity.object_id == "client_disconnected_event": + client_disconnected_event = entity + elif entity.object_id == "service_called": + service_called_sensor = entity + elif isinstance(entity, SensorInfo): + if entity.object_id == "service_argument_value": + service_arg_sensor = entity + elif isinstance(entity, TextSensorInfo): + if entity.object_id == "last_client_info": + last_client_info = entity + + # Verify all entities exist + assert client_connected is not None, "client_connected sensor not found" + assert client_disconnected_event is not None, ( + "client_disconnected_event sensor not found" + ) + assert service_called_sensor is not None, "service_called sensor not found" + assert service_arg_sensor is not None, "service_arg_sensor not found" + assert last_client_info is not None, "last_client_info sensor not found" + + # Verify services exist + assert len(services) == 2, f"Expected 2 services, found {len(services)}" + + # Find our services + simple_service: UserService | None = None + service_with_args: UserService | None = None + + for service in services: + if service.name == "test_simple_service": + simple_service = service + elif service.name == "test_service_with_args": + service_with_args = service + + assert simple_service is not None, "test_simple_service not found" + assert service_with_args is not None, "test_service_with_args not found" + + # Verify service arguments + assert len(service_with_args.args) == 4, ( + f"Expected 4 args, found {len(service_with_args.args)}" + ) + + # Check arg types + arg_types = {arg.name: arg.type for arg in service_with_args.args} + assert arg_types["arg_string"] == UserServiceArgType.STRING + assert arg_types["arg_int"] == UserServiceArgType.INT + assert arg_types["arg_bool"] == UserServiceArgType.BOOL + assert arg_types["arg_float"] == UserServiceArgType.FLOAT + + # Track state changes + states: dict[int, EntityState] = {} + states_future: asyncio.Future[None] = loop.create_future() + + def on_state(state: EntityState) -> None: + states[state.key] = state + # Check if we have initial states for connection sensors + if ( + client_connected.key in states + and last_client_info.key in states + and not states_future.done() + ): + states_future.set_result(None) + + client.subscribe_states(on_state) + + # Wait for initial states + await asyncio.wait_for(states_future, timeout=5.0) + + # Verify on_client_connected trigger fired + connected_state = states.get(client_connected.key) + assert connected_state is not None + assert connected_state.state is True, "Client should be connected" + + # Verify client info was captured + client_info_state = states.get(last_client_info.key) + assert client_info_state is not None + assert isinstance(client_info_state.state, str) + assert len(client_info_state.state) > 0, "Client info should not be empty" + + # Test simple service + service_future: asyncio.Future[None] = loop.create_future() + + def check_service_called(state: EntityState) -> None: + if state.key == service_called_sensor.key and state.state is True: + if not service_future.done(): + service_future.set_result(None) + + # Update callback to check for service execution + client.subscribe_states(check_service_called) + + # Call simple service + client.execute_service(simple_service, {}) + + # Wait for service to execute + await asyncio.wait_for(service_future, timeout=5.0) + + # Test service with arguments + arg_future: asyncio.Future[None] = loop.create_future() + expected_float = 42.5 + + def check_arg_sensor(state: EntityState) -> None: + if ( + state.key == service_arg_sensor.key + and abs(state.state - expected_float) < 0.01 + ): + if not arg_future.done(): + arg_future.set_result(None) + + client.subscribe_states(check_arg_sensor) + + # Call service with arguments + client.execute_service( + service_with_args, + { + "arg_string": "test_string", + "arg_int": 123, + "arg_bool": True, + "arg_float": expected_float, + }, + ) + + # Wait for service with args to execute + await asyncio.wait_for(arg_future, timeout=5.0) + + # After disconnecting first client, reconnect and verify triggers work + async with api_client_connected() as client2: + # Subscribe to states with new client + states2: dict[int, EntityState] = {} + connected_future: asyncio.Future[None] = loop.create_future() + + def on_state2(state: EntityState) -> None: + states2[state.key] = state + # Check for reconnection + if state.key == client_connected.key and state.state is True: + if not connected_future.done(): + connected_future.set_result(None) + + client2.subscribe_states(on_state2) + + # Wait for connected state + await asyncio.wait_for(connected_future, timeout=5.0) + + # Verify client is connected again (on_client_connected fired) + assert states2[client_connected.key].state is True, ( + "Client should be reconnected" + ) + + # The client_disconnected_event should be ON from when we disconnected + # (it was set ON by on_client_disconnected trigger) + disconnected_state = states2.get(client_disconnected_event.key) + assert disconnected_state is not None + assert disconnected_state.state is True, ( + "Disconnect event should be ON from previous disconnect" + ) From 140ca070a20c9e1b3d3d380f467c0553c2f8a9b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 22:40:36 -0500 Subject: [PATCH 176/293] Optimize scheduler string storage to eliminate heap allocations (#9251) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/core/component.cpp | 18 +- esphome/core/component.h | 34 ++++ esphome/core/scheduler.cpp | 175 +++++++++++++----- esphome/core/scheduler.h | 119 ++++++++++-- .../fixtures/scheduler_string_test.yaml | 164 ++++++++++++++++ .../integration/test_scheduler_string_test.py | 166 +++++++++++++++++ 6 files changed, 614 insertions(+), 62 deletions(-) create mode 100644 tests/integration/fixtures/scheduler_string_test.yaml create mode 100644 tests/integration/test_scheduler_string_test.py diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 8fa63de84e..6661223e35 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -60,10 +60,18 @@ void Component::set_interval(const std::string &name, uint32_t interval, std::fu App.scheduler.set_interval(this, name, interval, std::move(f)); } +void Component::set_interval(const char *name, uint32_t interval, std::function &&f) { // NOLINT + App.scheduler.set_interval(this, name, interval, std::move(f)); +} + bool Component::cancel_interval(const std::string &name) { // NOLINT return App.scheduler.cancel_interval(this, name); } +bool Component::cancel_interval(const char *name) { // NOLINT + return App.scheduler.cancel_interval(this, name); +} + void Component::set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, float backoff_increase_factor) { // NOLINT App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); @@ -77,10 +85,18 @@ void Component::set_timeout(const std::string &name, uint32_t timeout, std::func App.scheduler.set_timeout(this, name, timeout, std::move(f)); } +void Component::set_timeout(const char *name, uint32_t timeout, std::function &&f) { // NOLINT + App.scheduler.set_timeout(this, name, timeout, std::move(f)); +} + bool Component::cancel_timeout(const std::string &name) { // NOLINT return App.scheduler.cancel_timeout(this, name); } +bool Component::cancel_timeout(const char *name) { // NOLINT + return App.scheduler.cancel_timeout(this, name); +} + void Component::call_loop() { this->loop(); } void Component::call_setup() { this->setup(); } void Component::call_dump_config() { @@ -189,7 +205,7 @@ bool Component::is_in_loop_state() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP; } void Component::defer(std::function &&f) { // NOLINT - App.scheduler.set_timeout(this, "", 0, std::move(f)); + App.scheduler.set_timeout(this, static_cast(nullptr), 0, std::move(f)); } bool Component::cancel_defer(const std::string &name) { // NOLINT return App.scheduler.cancel_timeout(this, name); diff --git a/esphome/core/component.h b/esphome/core/component.h index 7f2bdd8414..5b37deeb68 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -260,6 +260,22 @@ class Component { */ void set_interval(const std::string &name, uint32_t interval, std::function &&f); // NOLINT + /** Set an interval function with a const char* name. + * + * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. + * This means the name should be: + * - A string literal (e.g., "update") + * - A static const char* variable + * - A pointer with lifetime >= the scheduled task + * + * For dynamic strings, use the std::string overload instead. + * + * @param name The identifier for this interval function (must have static lifetime) + * @param interval The interval in ms + * @param f The function to call + */ + void set_interval(const char *name, uint32_t interval, std::function &&f); // NOLINT + void set_interval(uint32_t interval, std::function &&f); // NOLINT /** Cancel an interval function. @@ -268,6 +284,7 @@ class Component { * @return Whether an interval functions was deleted. */ bool cancel_interval(const std::string &name); // NOLINT + bool cancel_interval(const char *name); // NOLINT /** Set an retry function with a unique name. Empty name means no cancelling possible. * @@ -328,6 +345,22 @@ class Component { */ void set_timeout(const std::string &name, uint32_t timeout, std::function &&f); // NOLINT + /** Set a timeout function with a const char* name. + * + * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. + * This means the name should be: + * - A string literal (e.g., "init") + * - A static const char* variable + * - A pointer with lifetime >= the timeout duration + * + * For dynamic strings, use the std::string overload instead. + * + * @param name The identifier for this timeout function (must have static lifetime) + * @param timeout The timeout in ms + * @param f The function to call + */ + void set_timeout(const char *name, uint32_t timeout, std::function &&f); // NOLINT + void set_timeout(uint32_t timeout, std::function &&f); // NOLINT /** Cancel a timeout function. @@ -336,6 +369,7 @@ class Component { * @return Whether a timeout functions was deleted. */ bool cancel_timeout(const std::string &name); // NOLINT + bool cancel_timeout(const char *name); // NOLINT /** Defer a callback to the next loop() call. * diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 8144435163..5c01b4f3f4 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -7,6 +7,7 @@ #include "esphome/core/log.h" #include #include +#include namespace esphome { @@ -17,75 +18,138 @@ static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; // Uncomment to debug scheduler // #define ESPHOME_DEBUG_SCHEDULER +#ifdef ESPHOME_DEBUG_SCHEDULER +// Helper to validate that a pointer looks like it's in static memory +static void validate_static_string(const char *name) { + if (name == nullptr) + return; + + // This is a heuristic check - stack and heap pointers are typically + // much higher in memory than static data + uintptr_t addr = reinterpret_cast(name); + + // Create a stack variable to compare against + int stack_var; + uintptr_t stack_addr = reinterpret_cast(&stack_var); + + // If the string pointer is near our stack variable, it's likely on the stack + // Using 8KB range as ESP32 main task stack is typically 8192 bytes + if (addr > (stack_addr - 0x2000) && addr < (stack_addr + 0x2000)) { + ESP_LOGW(TAG, + "WARNING: Scheduler name '%s' at %p appears to be on the stack - this is unsafe!\n" + " Stack reference at %p", + name, name, &stack_var); + } + + // Also check if it might be on the heap by seeing if it's in a very different range + // This is platform-specific but generally heap is allocated far from static memory + static const char *static_str = "test"; + uintptr_t static_addr = reinterpret_cast(static_str); + + // If the address is very far from known static memory, it might be heap + if (addr > static_addr + 0x100000 || (static_addr > 0x100000 && addr < static_addr - 0x100000)) { + ESP_LOGW(TAG, "WARNING: Scheduler name '%s' at %p might be on heap (static ref at %p)", name, name, static_str); + } +} +#endif + // A note on locking: the `lock_` lock protects the `items_` and `to_add_` containers. It must be taken when writing to // them (i.e. when adding/removing items, but not when changing items). As items are only deleted from the loop task, // iterating over them from the loop task is fine; but iterating from any other context requires the lock to be held to // avoid the main thread modifying the list while it is being accessed. -void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout, - std::function func) { - const auto now = this->millis_(); +// Common implementation for both timeout and interval +void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, + const void *name_ptr, uint32_t delay, std::function func) { + // Get the name as const char* + const char *name_cstr = + is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str(); - if (!name.empty()) - this->cancel_timeout(component, name); + // Cancel existing timer if name is not empty + if (name_cstr != nullptr && name_cstr[0] != '\0') { + this->cancel_item_(component, name_cstr, type); + } - if (timeout == SCHEDULER_DONT_RUN) + if (delay == SCHEDULER_DONT_RUN) return; + const auto now = this->millis_(); + + // Create and populate the scheduler item auto item = make_unique(); item->component = component; - item->name = name; - item->type = SchedulerItem::TIMEOUT; - item->next_execution_ = now + timeout; + item->set_name(name_cstr, !is_static_string); + item->type = type; item->callback = std::move(func); item->remove = false; + + // Type-specific setup + if (type == SchedulerItem::INTERVAL) { + item->interval = delay; + // Calculate random offset (0 to interval/2) + uint32_t offset = (delay != 0) ? (random_uint32() % delay) / 2 : 0; + item->next_execution_ = now + offset; + } else { + item->interval = 0; + item->next_execution_ = now + delay; + } + #ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "set_timeout(name='%s/%s', timeout=%" PRIu32 ")", item->get_source(), name.c_str(), timeout); + // Validate static strings in debug mode + if (is_static_string && name_cstr != nullptr) { + validate_static_string(name_cstr); + } + + // Debug logging + const char *type_str = (type == SchedulerItem::TIMEOUT) ? "timeout" : "interval"; + if (type == SchedulerItem::TIMEOUT) { + ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, item->get_source(), + name_cstr ? name_cstr : "(null)", type_str, delay); + } else { + ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, item->get_source(), + name_cstr ? name_cstr : "(null)", type_str, delay, static_cast(item->next_execution_ - now)); + } #endif + this->push_(std::move(item)); } + +void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function func) { + this->set_timer_common_(component, SchedulerItem::TIMEOUT, true, name, timeout, std::move(func)); +} + +void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout, + std::function func) { + this->set_timer_common_(component, SchedulerItem::TIMEOUT, false, &name, timeout, std::move(func)); +} bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name) { return this->cancel_item_(component, name, SchedulerItem::TIMEOUT); } +bool HOT Scheduler::cancel_timeout(Component *component, const char *name) { + return this->cancel_item_(component, name, SchedulerItem::TIMEOUT); +} void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval, std::function func) { - const auto now = this->millis_(); + this->set_timer_common_(component, SchedulerItem::INTERVAL, false, &name, interval, std::move(func)); +} - if (!name.empty()) - this->cancel_interval(component, name); - - if (interval == SCHEDULER_DONT_RUN) - return; - - // only put offset in lower half - uint32_t offset = 0; - if (interval != 0) - offset = (random_uint32() % interval) / 2; - - auto item = make_unique(); - item->component = component; - item->name = name; - item->type = SchedulerItem::INTERVAL; - item->interval = interval; - item->next_execution_ = now + offset; - item->callback = std::move(func); - item->remove = false; -#ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "set_interval(name='%s/%s', interval=%" PRIu32 ", offset=%" PRIu32 ")", item->get_source(), - name.c_str(), interval, offset); -#endif - this->push_(std::move(item)); +void HOT Scheduler::set_interval(Component *component, const char *name, uint32_t interval, + std::function func) { + this->set_timer_common_(component, SchedulerItem::INTERVAL, true, name, interval, std::move(func)); } bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) { return this->cancel_item_(component, name, SchedulerItem::INTERVAL); } +bool HOT Scheduler::cancel_interval(Component *component, const char *name) { + return this->cancel_item_(component, name, SchedulerItem::INTERVAL); +} struct RetryArgs { std::function func; uint8_t retry_countdown; uint32_t current_interval; Component *component; - std::string name; + std::string name; // Keep as std::string since retry uses it dynamically float backoff_increase_factor; Scheduler *scheduler; }; @@ -154,7 +218,7 @@ void HOT Scheduler::call() { if (now - last_print > 2000) { last_print = now; std::vector> old_items; - ESP_LOGD(TAG, "Items: count=%u, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_, + ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_, this->last_millis_); while (!this->empty_()) { this->lock_.lock(); @@ -162,8 +226,9 @@ void HOT Scheduler::call() { this->pop_raw_(); this->lock_.unlock(); + const char *name = item->get_name(); ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64, - item->get_type_str(), item->get_source(), item->name.c_str(), item->interval, + item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval, item->next_execution_ - now, item->next_execution_); old_items.push_back(std::move(item)); @@ -220,9 +285,10 @@ void HOT Scheduler::call() { App.set_current_component(item->component); #ifdef ESPHOME_DEBUG_SCHEDULER + const char *item_name = item->get_name(); ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", - item->get_type_str(), item->get_source(), item->name.c_str(), item->interval, item->next_execution_, - now); + item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval, + item->next_execution_, now); #endif // Warning: During callback(), a lot of stuff can happen, including: @@ -298,19 +364,33 @@ void HOT Scheduler::push_(std::unique_ptr item) { LockGuard guard{this->lock_}; this->to_add_.push_back(std::move(item)); } -bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) { +// Common implementation for cancel operations +bool HOT Scheduler::cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr, + SchedulerItem::Type type) { + // Get the name as const char* + const char *name_cstr = + is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str(); + + // Handle null or empty names + if (name_cstr == nullptr) + return false; + // obtain lock because this function iterates and can be called from non-loop task context LockGuard guard{this->lock_}; bool ret = false; + for (auto &it : this->items_) { - if (it->component == component && it->name == name && it->type == type && !it->remove) { + const char *item_name = it->get_name(); + if (it->component == component && item_name != nullptr && strcmp(name_cstr, item_name) == 0 && it->type == type && + !it->remove) { to_remove_++; it->remove = true; ret = true; } } for (auto &it : this->to_add_) { - if (it->component == component && it->name == name && it->type == type) { + const char *item_name = it->get_name(); + if (it->component == component && item_name != nullptr && strcmp(name_cstr, item_name) == 0 && it->type == type) { it->remove = true; ret = true; } @@ -318,6 +398,15 @@ bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, return ret; } + +bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) { + return this->cancel_item_common_(component, false, &name, type); +} + +bool HOT Scheduler::cancel_item_(Component *component, const char *name, SchedulerItem::Type type) { + return this->cancel_item_common_(component, true, name, type); +} + uint64_t Scheduler::millis_() { // Get the current 32-bit millis value const uint32_t now = millis(); diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 1284bcd4a7..a64968932e 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -12,11 +12,40 @@ class Component; class Scheduler { public: + // Public API - accepts std::string for backward compatibility void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function func); - bool cancel_timeout(Component *component, const std::string &name); - void set_interval(Component *component, const std::string &name, uint32_t interval, std::function func); - bool cancel_interval(Component *component, const std::string &name); + /** Set a timeout with a const char* name. + * + * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. + * This means the name should be: + * - A string literal (e.g., "update") + * - A static const char* variable + * - A pointer with lifetime >= the scheduled task + * + * For dynamic strings, use the std::string overload instead. + */ + void set_timeout(Component *component, const char *name, uint32_t timeout, std::function func); + + bool cancel_timeout(Component *component, const std::string &name); + bool cancel_timeout(Component *component, const char *name); + + void set_interval(Component *component, const std::string &name, uint32_t interval, std::function func); + + /** Set an interval with a const char* name. + * + * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. + * This means the name should be: + * - A string literal (e.g., "update") + * - A static const char* variable + * - A pointer with lifetime >= the scheduled task + * + * For dynamic strings, use the std::string overload instead. + */ + void set_interval(Component *component, const char *name, uint32_t interval, std::function func); + + bool cancel_interval(Component *component, const std::string &name); + bool cancel_interval(Component *component, const char *name); void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, std::function func, float backoff_increase_factor = 1.0f); bool cancel_retry(Component *component, const std::string &name); @@ -36,32 +65,86 @@ class Scheduler { // with a 16-bit rollover counter to create a 64-bit time that won't roll over for // billions of years. This ensures correct scheduling even when devices run for months. uint64_t next_execution_; - std::string name; - std::function callback; - enum Type : uint8_t { TIMEOUT, INTERVAL } type; - bool remove; - static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); - const char *get_type_str() { - switch (this->type) { - case SchedulerItem::INTERVAL: - return "interval"; - case SchedulerItem::TIMEOUT: - return "timeout"; - default: - return ""; + // Optimized name storage using tagged union + union { + const char *static_name; // For string literals (no allocation) + char *dynamic_name; // For allocated strings + } name_; + + std::function callback; + + // Bit-packed fields to minimize padding + enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; + bool remove : 1; + bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[]) + // 5 bits padding + + // Constructor + SchedulerItem() + : component(nullptr), interval(0), next_execution_(0), type(TIMEOUT), remove(false), name_is_dynamic(false) { + name_.static_name = nullptr; + } + + // Destructor to clean up dynamic names + ~SchedulerItem() { + if (name_is_dynamic) { + delete[] name_.dynamic_name; } } - const char *get_source() { - return this->component != nullptr ? this->component->get_component_source() : "unknown"; + + // Delete copy operations to prevent accidental copies + SchedulerItem(const SchedulerItem &) = delete; + SchedulerItem &operator=(const SchedulerItem &) = delete; + + // Default move operations + SchedulerItem(SchedulerItem &&) = default; + SchedulerItem &operator=(SchedulerItem &&) = default; + + // Helper to get the name regardless of storage type + const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; } + + // Helper to set name with proper ownership + void set_name(const char *name, bool make_copy = false) { + // Clean up old dynamic name if any + if (name_is_dynamic && name_.dynamic_name) { + delete[] name_.dynamic_name; + name_is_dynamic = false; + } + + if (!name || !name[0]) { + name_.static_name = nullptr; + } else if (make_copy) { + // Make a copy for dynamic strings + size_t len = strlen(name); + name_.dynamic_name = new char[len + 1]; + memcpy(name_.dynamic_name, name, len + 1); + name_is_dynamic = true; + } else { + // Use static string directly + name_.static_name = name; + } } + + static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); + const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; } + const char *get_source() const { return component ? component->get_component_source() : "unknown"; } }; + // Common implementation for both timeout and interval + void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr, + uint32_t delay, std::function func); + uint64_t millis_(); void cleanup_(); void pop_raw_(); void push_(std::unique_ptr item); + // Common implementation for cancel operations + bool cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type); + bool cancel_item_(Component *component, const std::string &name, SchedulerItem::Type type); + bool cancel_item_(Component *component, const char *name, SchedulerItem::Type type); + bool empty_() { this->cleanup_(); return this->items_.empty(); diff --git a/tests/integration/fixtures/scheduler_string_test.yaml b/tests/integration/fixtures/scheduler_string_test.yaml new file mode 100644 index 0000000000..1188577e15 --- /dev/null +++ b/tests/integration/fixtures/scheduler_string_test.yaml @@ -0,0 +1,164 @@ +esphome: + name: scheduler-string-test + on_boot: + priority: -100 + then: + - logger.log: "Starting scheduler string tests" + platformio_options: + build_flags: + - "-DESPHOME_DEBUG_SCHEDULER" # Enable scheduler debug logging + +host: +api: +logger: + level: VERBOSE + +globals: + - id: timeout_counter + type: int + initial_value: '0' + - id: interval_counter + type: int + initial_value: '0' + - id: dynamic_counter + type: int + initial_value: '0' + - id: static_tests_done + type: bool + initial_value: 'false' + - id: dynamic_tests_done + type: bool + initial_value: 'false' + - id: results_reported + type: bool + initial_value: 'false' + +script: + - id: test_static_strings + then: + - logger.log: "Testing static string timeouts and intervals" + - lambda: |- + auto *component1 = id(test_sensor1); + // Test 1: Static string literals with set_timeout + App.scheduler.set_timeout(component1, "static_timeout_1", 50, []() { + ESP_LOGI("test", "Static timeout 1 fired"); + id(timeout_counter) += 1; + }); + + // Test 2: Static const char* with set_timeout + static const char* TIMEOUT_NAME = "static_timeout_2"; + App.scheduler.set_timeout(component1, TIMEOUT_NAME, 100, []() { + ESP_LOGI("test", "Static timeout 2 fired"); + id(timeout_counter) += 1; + }); + + // Test 3: Static string literal with set_interval + App.scheduler.set_interval(component1, "static_interval_1", 200, []() { + ESP_LOGI("test", "Static interval 1 fired, count: %d", id(interval_counter)); + id(interval_counter) += 1; + if (id(interval_counter) >= 3) { + App.scheduler.cancel_interval(id(test_sensor1), "static_interval_1"); + ESP_LOGI("test", "Cancelled static interval 1"); + } + }); + + // Test 4: Empty string (should be handled safely) + App.scheduler.set_timeout(component1, "", 150, []() { + ESP_LOGI("test", "Empty string timeout fired"); + }); + + // Test 5: Cancel timeout with const char* literal + App.scheduler.set_timeout(component1, "cancel_static_timeout", 5000, []() { + ESP_LOGI("test", "This static timeout should be cancelled"); + }); + // Cancel using const char* directly + App.scheduler.cancel_timeout(component1, "cancel_static_timeout"); + ESP_LOGI("test", "Cancelled static timeout using const char*"); + + - id: test_dynamic_strings + then: + - logger.log: "Testing dynamic string timeouts and intervals" + - lambda: |- + auto *component2 = id(test_sensor2); + + // Test 6: Dynamic string with set_timeout (std::string) + std::string dynamic_name = "dynamic_timeout_" + std::to_string(id(dynamic_counter)++); + App.scheduler.set_timeout(component2, dynamic_name, 100, []() { + ESP_LOGI("test", "Dynamic timeout fired"); + id(timeout_counter) += 1; + }); + + // Test 7: Dynamic string with set_interval + std::string interval_name = "dynamic_interval_" + std::to_string(id(dynamic_counter)++); + App.scheduler.set_interval(component2, interval_name, 250, [interval_name]() { + ESP_LOGI("test", "Dynamic interval fired: %s", interval_name.c_str()); + id(interval_counter) += 1; + if (id(interval_counter) >= 6) { + App.scheduler.cancel_interval(id(test_sensor2), interval_name); + ESP_LOGI("test", "Cancelled dynamic interval"); + } + }); + + // Test 8: Cancel with different string object but same content + std::string cancel_name = "cancel_test"; + App.scheduler.set_timeout(component2, cancel_name, 2000, []() { + ESP_LOGI("test", "This should be cancelled"); + }); + + // Cancel using a different string object + std::string cancel_name_2 = "cancel_test"; + App.scheduler.cancel_timeout(component2, cancel_name_2); + ESP_LOGI("test", "Cancelled timeout using different string object"); + + - id: report_results + then: + - lambda: |- + ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d", + id(timeout_counter), id(interval_counter)); + +sensor: + - platform: template + name: Test Sensor 1 + id: test_sensor1 + lambda: return 1.0; + update_interval: never + + - platform: template + name: Test Sensor 2 + id: test_sensor2 + lambda: return 2.0; + update_interval: never + +interval: + # Run static string tests after boot - using script to run once + - interval: 0.1s + then: + - if: + condition: + lambda: 'return id(static_tests_done) == false;' + then: + - lambda: 'id(static_tests_done) = true;' + - script.execute: test_static_strings + - logger.log: "Started static string tests" + + # Run dynamic string tests after static tests + - interval: 0.2s + then: + - if: + condition: + lambda: 'return id(static_tests_done) && !id(dynamic_tests_done);' + then: + - lambda: 'id(dynamic_tests_done) = true;' + - delay: 0.2s + - script.execute: test_dynamic_strings + + # Report results after all tests + - interval: 0.2s + then: + - if: + condition: + lambda: 'return id(dynamic_tests_done) && !id(results_reported);' + then: + - lambda: 'id(results_reported) = true;' + - delay: 1s + - script.execute: report_results diff --git a/tests/integration/test_scheduler_string_test.py b/tests/integration/test_scheduler_string_test.py new file mode 100644 index 0000000000..b5ca07f9db --- /dev/null +++ b/tests/integration/test_scheduler_string_test.py @@ -0,0 +1,166 @@ +"""Test scheduler string optimization with static and dynamic strings.""" + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_string_test( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that scheduler handles both static and dynamic strings correctly.""" + # Track counts + timeout_count = 0 + interval_count = 0 + + # Events for each test completion + static_timeout_1_fired = asyncio.Event() + static_timeout_2_fired = asyncio.Event() + static_interval_fired = asyncio.Event() + static_interval_cancelled = asyncio.Event() + empty_string_timeout_fired = asyncio.Event() + static_timeout_cancelled = asyncio.Event() + dynamic_timeout_fired = asyncio.Event() + dynamic_interval_fired = asyncio.Event() + cancel_test_done = asyncio.Event() + final_results_logged = asyncio.Event() + + # Track interval counts + static_interval_count = 0 + dynamic_interval_count = 0 + + def on_log_line(line: str) -> None: + nonlocal \ + timeout_count, \ + interval_count, \ + static_interval_count, \ + dynamic_interval_count + + # Strip ANSI color codes + clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) + + # Check for static timeout completions + if "Static timeout 1 fired" in clean_line: + static_timeout_1_fired.set() + timeout_count += 1 + + elif "Static timeout 2 fired" in clean_line: + static_timeout_2_fired.set() + timeout_count += 1 + + # Check for static interval + elif "Static interval 1 fired" in clean_line: + match = re.search(r"count: (\d+)", clean_line) + if match: + static_interval_count = int(match.group(1)) + static_interval_fired.set() + + elif "Cancelled static interval 1" in clean_line: + static_interval_cancelled.set() + + # Check for empty string timeout + elif "Empty string timeout fired" in clean_line: + empty_string_timeout_fired.set() + + # Check for static timeout cancellation + elif "Cancelled static timeout using const char*" in clean_line: + static_timeout_cancelled.set() + + # Check for dynamic string tests + elif "Dynamic timeout fired" in clean_line: + dynamic_timeout_fired.set() + timeout_count += 1 + + elif "Dynamic interval fired" in clean_line: + dynamic_interval_count += 1 + dynamic_interval_fired.set() + + # Check for cancel test + elif "Cancelled timeout using different string object" in clean_line: + cancel_test_done.set() + + # Check for final results + elif "Final results" in clean_line: + match = re.search(r"Timeouts: (\d+), Intervals: (\d+)", clean_line) + if match: + timeout_count = int(match.group(1)) + interval_count = int(match.group(2)) + final_results_logged.set() + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-string-test" + + # Wait for static string tests + try: + await asyncio.wait_for(static_timeout_1_fired.wait(), timeout=0.5) + except asyncio.TimeoutError: + pytest.fail("Static timeout 1 did not fire within 0.5 seconds") + + try: + await asyncio.wait_for(static_timeout_2_fired.wait(), timeout=0.5) + except asyncio.TimeoutError: + pytest.fail("Static timeout 2 did not fire within 0.5 seconds") + + try: + await asyncio.wait_for(static_interval_fired.wait(), timeout=1.0) + except asyncio.TimeoutError: + pytest.fail("Static interval did not fire within 1 second") + + try: + await asyncio.wait_for(static_interval_cancelled.wait(), timeout=2.0) + except asyncio.TimeoutError: + pytest.fail("Static interval was not cancelled within 2 seconds") + + # Verify static interval ran at least 3 times + assert static_interval_count >= 2, ( + f"Expected static interval to run at least 3 times, got {static_interval_count + 1}" + ) + + # Verify static timeout was cancelled + assert static_timeout_cancelled.is_set(), ( + "Static timeout should have been cancelled" + ) + + # Wait for dynamic string tests + try: + await asyncio.wait_for(dynamic_timeout_fired.wait(), timeout=1.0) + except asyncio.TimeoutError: + pytest.fail("Dynamic timeout did not fire within 1 second") + + try: + await asyncio.wait_for(dynamic_interval_fired.wait(), timeout=1.5) + except asyncio.TimeoutError: + pytest.fail("Dynamic interval did not fire within 1.5 seconds") + + # Wait for cancel test + try: + await asyncio.wait_for(cancel_test_done.wait(), timeout=1.0) + except asyncio.TimeoutError: + pytest.fail("Cancel test did not complete within 1 second") + + # Wait for final results + try: + await asyncio.wait_for(final_results_logged.wait(), timeout=4.0) + except asyncio.TimeoutError: + pytest.fail("Final results were not logged within 4 seconds") + + # Verify results + assert timeout_count >= 3, f"Expected at least 3 timeouts, got {timeout_count}" + assert interval_count >= 3, ( + f"Expected at least 3 interval fires, got {interval_count}" + ) + + # Empty string timeout DOES fire (scheduler accepts empty names) + assert empty_string_timeout_fired.is_set(), "Empty string timeout should fire" From 9890659f61cd2af1c54c9c1b9f8da06e4ebc1bca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 23:12:03 -0500 Subject: [PATCH 177/293] Optimize web_server UrlMatch to avoid heap allocations (#9263) --- esphome/components/web_server/web_server.cpp | 287 +++++++++++-------- esphome/components/web_server/web_server.h | 24 +- 2 files changed, 186 insertions(+), 125 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1bf3ed11cb..669bfbf279 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -49,26 +49,69 @@ static const char *const HEADER_CORS_ALLOW_PNA = "Access-Control-Allow-Private-N UrlMatch match_url(const std::string &url, bool only_domain = false) { UrlMatch match; match.valid = false; - size_t domain_end = url.find('/', 1); - if (domain_end == std::string::npos) + match.domain = nullptr; + match.id = nullptr; + match.method = nullptr; + match.domain_len = 0; + match.id_len = 0; + match.method_len = 0; + + const char *url_ptr = url.c_str(); + size_t url_len = url.length(); + + // URL must start with '/' + if (url_len < 2 || url_ptr[0] != '/') return match; - match.domain = url.substr(1, domain_end - 1); + + // Find domain + size_t domain_start = 1; + size_t domain_end = url.find('/', domain_start); + + if (domain_end == std::string::npos) { + // URL is just "/domain" + match.domain = url_ptr + domain_start; + match.domain_len = url_len - domain_start; + match.valid = true; + return match; + } + + // Set domain + match.domain = url_ptr + domain_start; + match.domain_len = domain_end - domain_start; + if (only_domain) { match.valid = true; return match; } - if (url.length() == domain_end - 1) + + // Check if there's anything after domain + if (url_len == domain_end + 1) return match; + + // Find ID size_t id_begin = domain_end + 1; size_t id_end = url.find('/', id_begin); + match.valid = true; + if (id_end == std::string::npos) { - match.id = url.substr(id_begin, url.length() - id_begin); + // URL is "/domain/id" with no method + match.id = url_ptr + id_begin; + match.id_len = url_len - id_begin; return match; } - match.id = url.substr(id_begin, id_end - id_begin); + + // Set ID + match.id = url_ptr + id_begin; + match.id_len = id_end - id_begin; + + // Set method if present size_t method_begin = id_end + 1; - match.method = url.substr(method_begin, url.length() - method_begin); + if (method_begin < url_len) { + match.method = url_ptr + method_begin; + match.method_len = url_len - method_begin; + } + return match; } @@ -386,9 +429,9 @@ void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { } void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (sensor::Sensor *obj : App.get_sensors()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -431,9 +474,9 @@ void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::s } void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (text_sensor::TextSensor *obj : App.get_text_sensors()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->text_sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -469,20 +512,20 @@ void WebServer::on_switch_update(switch_::Switch *obj, bool state) { } void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (switch_::Switch *obj : App.get_switches()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->switch_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method == "toggle") { + } else if (match.method_equals("toggle")) { this->schedule_([obj]() { obj->toggle(); }); request->send(200); - } else if (match.method == "turn_on") { + } else if (match.method_equals("turn_on")) { this->schedule_([obj]() { obj->turn_on(); }); request->send(200); - } else if (match.method == "turn_off") { + } else if (match.method_equals("turn_off")) { this->schedule_([obj]() { obj->turn_off(); }); request->send(200); } else { @@ -512,13 +555,13 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail #ifdef USE_BUTTON void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (button::Button *obj : App.get_buttons()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->button_json(obj, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method == "press") { + } else if (match.method_equals("press")) { this->schedule_([obj]() { obj->press(); }); request->send(200); return; @@ -553,9 +596,9 @@ void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { } void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->binary_sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -591,18 +634,18 @@ void WebServer::on_fan_update(fan::Fan *obj) { } void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (fan::Fan *obj : App.get_fans()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->fan_json(obj, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method == "toggle") { + } else if (match.method_equals("toggle")) { this->schedule_([obj]() { obj->toggle().perform(); }); request->send(200); - } else if (match.method == "turn_on" || match.method == "turn_off") { - auto call = match.method == "turn_on" ? obj->turn_on() : obj->turn_off(); + } else if (match.method_equals("turn_on") || match.method_equals("turn_off")) { + auto call = match.method_equals("turn_on") ? obj->turn_on() : obj->turn_off(); if (request->hasParam("speed_level")) { auto speed_level = request->getParam("speed_level")->value(); @@ -672,17 +715,17 @@ void WebServer::on_light_update(light::LightState *obj) { } void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (light::LightState *obj : App.get_lights()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->light_json(obj, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method == "toggle") { + } else if (match.method_equals("toggle")) { this->schedule_([obj]() { obj->toggle().perform(); }); request->send(200); - } else if (match.method == "turn_on") { + } else if (match.method_equals("turn_on")) { auto call = obj->turn_on(); if (request->hasParam("brightness")) { auto brightness = parse_number(request->getParam("brightness")->value().c_str()); @@ -739,7 +782,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa this->schedule_([call]() mutable { call.perform(); }); request->send(200); - } else if (match.method == "turn_off") { + } else if (match.method_equals("turn_off")) { auto call = obj->turn_off(); if (request->hasParam("transition")) { auto transition = parse_number(request->getParam("transition")->value().c_str()); @@ -788,10 +831,10 @@ void WebServer::on_cover_update(cover::Cover *obj) { } void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (cover::Cover *obj : App.get_covers()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->cover_json(obj, detail); request->send(200, "application/json", data.c_str()); @@ -799,15 +842,15 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa } auto call = obj->make_call(); - if (match.method == "open") { + if (match.method_equals("open")) { call.set_command_open(); - } else if (match.method == "close") { + } else if (match.method_equals("close")) { call.set_command_close(); - } else if (match.method == "stop") { + } else if (match.method_equals("stop")) { call.set_command_stop(); - } else if (match.method == "toggle") { + } else if (match.method_equals("toggle")) { call.set_command_toggle(); - } else if (match.method != "set") { + } else if (!match.method_equals("set")) { request->send(404); return; } @@ -869,16 +912,16 @@ void WebServer::on_number_update(number::Number *obj, float state) { } void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_numbers()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->number_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -940,15 +983,15 @@ void WebServer::on_date_update(datetime::DateEntity *obj) { } void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_dates()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->date_json(obj, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -999,15 +1042,15 @@ void WebServer::on_time_update(datetime::TimeEntity *obj) { } void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_times()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->time_json(obj, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -1057,15 +1100,15 @@ void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) { } void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_datetimes()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->datetime_json(obj, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -1116,16 +1159,16 @@ void WebServer::on_text_update(text::Text *obj, const std::string &state) { } void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_texts()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->text_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -1177,17 +1220,17 @@ void WebServer::on_select_update(select::Select *obj, const std::string &state, } void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_selects()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->select_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -1236,17 +1279,17 @@ void WebServer::on_climate_update(climate::Climate *obj) { } void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_climates()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->climate_json(obj, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -1395,20 +1438,20 @@ void WebServer::on_lock_update(lock::Lock *obj) { } void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (lock::Lock *obj : App.get_locks()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->lock_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method == "lock") { + } else if (match.method_equals("lock")) { this->schedule_([obj]() { obj->lock(); }); request->send(200); - } else if (match.method == "unlock") { + } else if (match.method_equals("unlock")) { this->schedule_([obj]() { obj->unlock(); }); request->send(200); - } else if (match.method == "open") { + } else if (match.method_equals("open")) { this->schedule_([obj]() { obj->open(); }); request->send(200); } else { @@ -1443,10 +1486,10 @@ void WebServer::on_valve_update(valve::Valve *obj) { } void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (valve::Valve *obj : App.get_valves()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->valve_json(obj, detail); request->send(200, "application/json", data.c_str()); @@ -1454,15 +1497,15 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa } auto call = obj->make_call(); - if (match.method == "open") { + if (match.method_equals("open")) { call.set_command_open(); - } else if (match.method == "close") { + } else if (match.method_equals("close")) { call.set_command_close(); - } else if (match.method == "stop") { + } else if (match.method_equals("stop")) { call.set_command_stop(); - } else if (match.method == "toggle") { + } else if (match.method_equals("toggle")) { call.set_command_toggle(); - } else if (match.method != "set") { + } else if (!match.method_equals("set")) { request->send(404); return; } @@ -1515,10 +1558,10 @@ void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlP } void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (alarm_control_panel::AlarmControlPanel *obj : App.get_alarm_control_panels()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->alarm_control_panel_json(obj, obj->get_state(), detail); request->send(200, "application/json", data.c_str()); @@ -1530,15 +1573,15 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques call.set_code(request->getParam("code")->value().c_str()); // NOLINT } - if (match.method == "disarm") { + if (match.method_equals("disarm")) { call.disarm(); - } else if (match.method == "arm_away") { + } else if (match.method_equals("arm_away")) { call.arm_away(); - } else if (match.method == "arm_home") { + } else if (match.method_equals("arm_home")) { call.arm_home(); - } else if (match.method == "arm_night") { + } else if (match.method_equals("arm_night")) { call.arm_night(); - } else if (match.method == "arm_vacation") { + } else if (match.method_equals("arm_vacation")) { call.arm_vacation(); } else { request->send(404); @@ -1582,10 +1625,10 @@ void WebServer::on_event(event::Event *obj, const std::string &event_type) { void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (event::Event *obj : App.get_events()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->event_json(obj, "", detail); request->send(200, "application/json", data.c_str()); @@ -1631,17 +1674,17 @@ void WebServer::on_update(update::UpdateEntity *obj) { } void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (update::UpdateEntity *obj : App.get_updates()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->update_json(obj, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "install") { + if (!match.method_equals("install")) { request->send(404); return; } @@ -1717,102 +1760,102 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const { if (!match.valid) return false; #ifdef USE_SENSOR - if (request->method() == HTTP_GET && match.domain == "sensor") + if (request->method() == HTTP_GET && match.domain_equals("sensor")) return true; #endif #ifdef USE_SWITCH - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "switch") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("switch")) return true; #endif #ifdef USE_BUTTON - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "button") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("button")) return true; #endif #ifdef USE_BINARY_SENSOR - if (request->method() == HTTP_GET && match.domain == "binary_sensor") + if (request->method() == HTTP_GET && match.domain_equals("binary_sensor")) return true; #endif #ifdef USE_FAN - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "fan") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("fan")) return true; #endif #ifdef USE_LIGHT - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "light") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("light")) return true; #endif #ifdef USE_TEXT_SENSOR - if (request->method() == HTTP_GET && match.domain == "text_sensor") + if (request->method() == HTTP_GET && match.domain_equals("text_sensor")) return true; #endif #ifdef USE_COVER - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "cover") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("cover")) return true; #endif #ifdef USE_NUMBER - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "number") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("number")) return true; #endif #ifdef USE_DATETIME_DATE - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "date") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("date")) return true; #endif #ifdef USE_DATETIME_TIME - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "time") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("time")) return true; #endif #ifdef USE_DATETIME_DATETIME - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "datetime") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("datetime")) return true; #endif #ifdef USE_TEXT - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "text") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("text")) return true; #endif #ifdef USE_SELECT - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "select") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("select")) return true; #endif #ifdef USE_CLIMATE - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "climate") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("climate")) return true; #endif #ifdef USE_LOCK - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "lock") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("lock")) return true; #endif #ifdef USE_VALVE - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "valve") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("valve")) return true; #endif #ifdef USE_ALARM_CONTROL_PANEL - if ((request->method() == HTTP_GET || request->method() == HTTP_POST) && match.domain == "alarm_control_panel") + if ((request->method() == HTTP_GET || request->method() == HTTP_POST) && match.domain_equals("alarm_control_panel")) return true; #endif #ifdef USE_EVENT - if (request->method() == HTTP_GET && match.domain == "event") + if (request->method() == HTTP_GET && match.domain_equals("event")) return true; #endif #ifdef USE_UPDATE - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "update") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("update")) return true; #endif @@ -1854,112 +1897,112 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { UrlMatch match = match_url(request->url().c_str()); // NOLINT #ifdef USE_SENSOR - if (match.domain == "sensor") { + if (match.domain_equals("sensor")) { this->handle_sensor_request(request, match); return; } #endif #ifdef USE_SWITCH - if (match.domain == "switch") { + if (match.domain_equals("switch")) { this->handle_switch_request(request, match); return; } #endif #ifdef USE_BUTTON - if (match.domain == "button") { + if (match.domain_equals("button")) { this->handle_button_request(request, match); return; } #endif #ifdef USE_BINARY_SENSOR - if (match.domain == "binary_sensor") { + if (match.domain_equals("binary_sensor")) { this->handle_binary_sensor_request(request, match); return; } #endif #ifdef USE_FAN - if (match.domain == "fan") { + if (match.domain_equals("fan")) { this->handle_fan_request(request, match); return; } #endif #ifdef USE_LIGHT - if (match.domain == "light") { + if (match.domain_equals("light")) { this->handle_light_request(request, match); return; } #endif #ifdef USE_TEXT_SENSOR - if (match.domain == "text_sensor") { + if (match.domain_equals("text_sensor")) { this->handle_text_sensor_request(request, match); return; } #endif #ifdef USE_COVER - if (match.domain == "cover") { + if (match.domain_equals("cover")) { this->handle_cover_request(request, match); return; } #endif #ifdef USE_NUMBER - if (match.domain == "number") { + if (match.domain_equals("number")) { this->handle_number_request(request, match); return; } #endif #ifdef USE_DATETIME_DATE - if (match.domain == "date") { + if (match.domain_equals("date")) { this->handle_date_request(request, match); return; } #endif #ifdef USE_DATETIME_TIME - if (match.domain == "time") { + if (match.domain_equals("time")) { this->handle_time_request(request, match); return; } #endif #ifdef USE_DATETIME_DATETIME - if (match.domain == "datetime") { + if (match.domain_equals("datetime")) { this->handle_datetime_request(request, match); return; } #endif #ifdef USE_TEXT - if (match.domain == "text") { + if (match.domain_equals("text")) { this->handle_text_request(request, match); return; } #endif #ifdef USE_SELECT - if (match.domain == "select") { + if (match.domain_equals("select")) { this->handle_select_request(request, match); return; } #endif #ifdef USE_CLIMATE - if (match.domain == "climate") { + if (match.domain_equals("climate")) { this->handle_climate_request(request, match); return; } #endif #ifdef USE_LOCK - if (match.domain == "lock") { + if (match.domain_equals("lock")) { this->handle_lock_request(request, match); return; @@ -1967,14 +2010,14 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { #endif #ifdef USE_VALVE - if (match.domain == "valve") { + if (match.domain_equals("valve")) { this->handle_valve_request(request, match); return; } #endif #ifdef USE_ALARM_CONTROL_PANEL - if (match.domain == "alarm_control_panel") { + if (match.domain_equals("alarm_control_panel")) { this->handle_alarm_control_panel_request(request, match); return; @@ -1982,7 +2025,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { #endif #ifdef USE_UPDATE - if (match.domain == "update") { + if (match.domain_equals("update")) { this->handle_update_request(request, match); return; } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 3be99eebae..991bca6fa7 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -40,10 +40,28 @@ namespace web_server { /// Internal helper struct that is used to parse incoming URLs struct UrlMatch { - std::string domain; ///< The domain of the component, for example "sensor" - std::string id; ///< The id of the device that's being accessed, for example "living_room_fan" - std::string method; ///< The method that's being called, for example "turn_on" + const char *domain; ///< Pointer to domain within URL, for example "sensor" + const char *id; ///< Pointer to id within URL, for example "living_room_fan" + const char *method; ///< Pointer to method within URL, for example "turn_on" + uint8_t domain_len; ///< Length of domain string + uint8_t id_len; ///< Length of id string + uint8_t method_len; ///< Length of method string bool valid; ///< Whether this match is valid + + // Helper methods for string comparisons + bool domain_equals(const char *str) const { + return domain && domain_len == strlen(str) && memcmp(domain, str, domain_len) == 0; + } + + bool id_equals(const std::string &str) const { + return id && id_len == str.length() && memcmp(id, str.c_str(), id_len) == 0; + } + + bool method_equals(const char *str) const { + return method && method_len == strlen(str) && memcmp(method, str, method_len) == 0; + } + + bool method_empty() const { return method_len == 0; } }; #ifdef USE_WEBSERVER_SORTING From d86f319d66918c0bcb729ad41801fda0f7a9db8c Mon Sep 17 00:00:00 2001 From: lamauny <57617527+lamauny@users.noreply.github.com> Date: Mon, 30 Jun 2025 06:20:36 +0200 Subject: [PATCH 178/293] Add support for LN882X Family (with LibreTiny) (#8954) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/__main__.py | 4 +- esphome/components/api/api_connection.cpp | 2 + esphome/components/async_tcp/__init__.py | 11 +- esphome/components/captive_portal/__init__.py | 11 +- esphome/components/esphome/ota/__init__.py | 1 + esphome/components/libretiny/const.py | 5 + .../libretiny/generate_components.py | 1 + esphome/components/ln882x/__init__.py | 52 ++++ esphome/components/ln882x/boards.py | 285 ++++++++++++++++++ esphome/components/logger/__init__.py | 10 +- .../components/remote_receiver/__init__.py | 1 + esphome/components/sntp/time.py | 2 + esphome/components/socket/__init__.py | 1 + esphome/components/web_server/__init__.py | 12 +- esphome/components/wifi/__init__.py | 1 + esphome/const.py | 1 + esphome/core/__init__.py | 7 +- esphome/dashboard/web_server.py | 10 +- esphome/wizard.py | 19 +- platformio.ini | 13 +- tests/components/adc/test.ln882x-ard.yaml | 4 + .../binary_sensor/test.ln882x-ard.yaml | 2 + tests/components/debug/test.ln882x-ard.yaml | 1 + .../homeassistant/test.ln882x-ard.yaml | 2 + tests/components/script/test.ln882x-ard.yaml | 1 + tests/components/sntp/test.ln882x-ard.yaml | 1 + tests/components/switch/test.ln882x-ard.yaml | 2 + tests/components/syslog/test.ln882x-ard.yaml | 1 + .../components/template/test.ln882x-ard.yaml | 2 + .../build_components_base.ln882x-ard.yaml | 15 + tests/unit_tests/test_config_validation.py | 7 +- tests/unit_tests/test_wizard.py | 22 ++ 33 files changed, 495 insertions(+), 15 deletions(-) create mode 100644 esphome/components/ln882x/__init__.py create mode 100644 esphome/components/ln882x/boards.py create mode 100644 tests/components/adc/test.ln882x-ard.yaml create mode 100644 tests/components/binary_sensor/test.ln882x-ard.yaml create mode 100644 tests/components/debug/test.ln882x-ard.yaml create mode 100644 tests/components/homeassistant/test.ln882x-ard.yaml create mode 100644 tests/components/script/test.ln882x-ard.yaml create mode 100644 tests/components/sntp/test.ln882x-ard.yaml create mode 100644 tests/components/switch/test.ln882x-ard.yaml create mode 100644 tests/components/syslog/test.ln882x-ard.yaml create mode 100644 tests/components/template/test.ln882x-ard.yaml create mode 100644 tests/test_build_components/build_components_base.ln882x-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index b3c66c775b..68c8684024 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -248,6 +248,7 @@ esphome/components/libretiny_pwm/* @kuba2k2 esphome/components/light/* @esphome/core esphome/components/lightwaverf/* @max246 esphome/components/lilygo_t5_47/touchscreen/* @jesserockz +esphome/components/ln882x/* @lamauny esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core esphome/components/logger/select/* @clydebarrow diff --git a/esphome/__main__.py b/esphome/__main__.py index 2dbdfeb1ff..d8a79c018a 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -34,11 +34,9 @@ from esphome.const import ( CONF_PORT, CONF_SUBSTITUTIONS, CONF_TOPIC, - PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, - PLATFORM_RTL87XX, SECRETS_FILES, ) from esphome.core import CORE, EsphomeError, coroutine @@ -354,7 +352,7 @@ def upload_program(config, args, host): if CORE.target_platform in (PLATFORM_RP2040): return upload_using_platformio(config, args.device) - if CORE.target_platform in (PLATFORM_BK72XX, PLATFORM_RTL87XX): + if CORE.is_libretiny: return upload_using_platformio(config, host) return 1 # Unknown target platform diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 6a40f21f99..b7624221c9 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1537,6 +1537,8 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { resp.manufacturer = "Raspberry Pi"; #elif defined(USE_BK72XX) resp.manufacturer = "Beken"; +#elif defined(USE_LN882X) + resp.manufacturer = "Lightning"; #elif defined(USE_RTL87XX) resp.manufacturer = "Realtek"; #elif defined(USE_HOST) diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index eec6a0e327..29097ce1b6 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -5,6 +5,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_LN882X, PLATFORM_RTL87XX, ) from esphome.core import CORE, coroutine_with_priority @@ -14,7 +15,15 @@ CODEOWNERS = ["@OttoWinter"] CONFIG_SCHEMA = cv.All( cv.Schema({}), cv.only_with_arduino, - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]), + cv.only_on( + [ + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_BK72XX, + PLATFORM_LN882X, + PLATFORM_RTL87XX, + ] + ), ) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index a55887948d..cba3b4921a 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_LN882X, PLATFORM_RTL87XX, ) from esphome.core import CORE, coroutine_with_priority @@ -27,7 +28,15 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]), + cv.only_on( + [ + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_BK72XX, + PLATFORM_LN882X, + PLATFORM_RTL87XX, + ] + ), ) diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 86006e3e18..901657ec82 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -100,6 +100,7 @@ CONFIG_SCHEMA = ( esp32=3232, rp2040=2040, bk72xx=8892, + ln882x=8820, rtl87xx=8892, ): cv.port, cv.Optional(CONF_PASSWORD): cv.string, diff --git a/esphome/components/libretiny/const.py b/esphome/components/libretiny/const.py index 362609df44..671992f8bd 100644 --- a/esphome/components/libretiny/const.py +++ b/esphome/components/libretiny/const.py @@ -50,6 +50,7 @@ KEY_FAMILY = "family" # COMPONENTS - auto-generated! Do not modify this block. COMPONENT_BK72XX = "bk72xx" +COMPONENT_LN882X = "ln882x" COMPONENT_RTL87XX = "rtl87xx" # COMPONENTS - end @@ -58,6 +59,7 @@ FAMILY_BK7231N = "BK7231N" FAMILY_BK7231Q = "BK7231Q" FAMILY_BK7231T = "BK7231T" FAMILY_BK7251 = "BK7251" +FAMILY_LN882H = "LN882H" FAMILY_RTL8710B = "RTL8710B" FAMILY_RTL8720C = "RTL8720C" FAMILIES = [ @@ -65,6 +67,7 @@ FAMILIES = [ FAMILY_BK7231Q, FAMILY_BK7231T, FAMILY_BK7251, + FAMILY_LN882H, FAMILY_RTL8710B, FAMILY_RTL8720C, ] @@ -73,6 +76,7 @@ FAMILY_FRIENDLY = { FAMILY_BK7231Q: "BK7231Q", FAMILY_BK7231T: "BK7231T", FAMILY_BK7251: "BK7251", + FAMILY_LN882H: "LN882H", FAMILY_RTL8710B: "RTL8710B", FAMILY_RTL8720C: "RTL8720C", } @@ -81,6 +85,7 @@ FAMILY_COMPONENT = { FAMILY_BK7231Q: COMPONENT_BK72XX, FAMILY_BK7231T: COMPONENT_BK72XX, FAMILY_BK7251: COMPONENT_BK72XX, + FAMILY_LN882H: COMPONENT_LN882X, FAMILY_RTL8710B: COMPONENT_RTL87XX, FAMILY_RTL8720C: COMPONENT_RTL87XX, } diff --git a/esphome/components/libretiny/generate_components.py b/esphome/components/libretiny/generate_components.py index ae55fd9e40..c750b79317 100644 --- a/esphome/components/libretiny/generate_components.py +++ b/esphome/components/libretiny/generate_components.py @@ -94,6 +94,7 @@ PIN_SCHEMA_EXTRA = f"libretiny.BASE_PIN_SCHEMA.extend({VAR_PIN_SCHEMA})" COMPONENT_MAP = { "rtl87xx": "realtek-amb", "bk72xx": "beken-72xx", + "ln882x": "lightning-ln882x", } diff --git a/esphome/components/ln882x/__init__.py b/esphome/components/ln882x/__init__.py new file mode 100644 index 0000000000..6a76218f87 --- /dev/null +++ b/esphome/components/ln882x/__init__.py @@ -0,0 +1,52 @@ +# This file was auto-generated by libretiny/generate_components.py +# Do not modify its contents. +# For custom pin validators, put validate_pin() or validate_usage() +# in gpio.py file in this directory. +# For changing schema/pin schema, put COMPONENT_SCHEMA or COMPONENT_PIN_SCHEMA +# in schema.py file in this directory. + +from esphome import pins +from esphome.components import libretiny +from esphome.components.libretiny.const import ( + COMPONENT_LN882X, + KEY_COMPONENT_DATA, + KEY_LIBRETINY, + LibreTinyComponent, +) +from esphome.core import CORE + +from .boards import LN882X_BOARD_PINS, LN882X_BOARDS + +CODEOWNERS = ["@lamauny"] +AUTO_LOAD = ["libretiny"] +IS_TARGET_PLATFORM = True + +COMPONENT_DATA = LibreTinyComponent( + name=COMPONENT_LN882X, + boards=LN882X_BOARDS, + board_pins=LN882X_BOARD_PINS, + pin_validation=None, + usage_validation=None, +) + + +def _set_core_data(config): + CORE.data[KEY_LIBRETINY] = {} + CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] = COMPONENT_DATA + return config + + +CONFIG_SCHEMA = libretiny.BASE_SCHEMA + +PIN_SCHEMA = libretiny.gpio.BASE_PIN_SCHEMA + +CONFIG_SCHEMA.prepend_extra(_set_core_data) + + +async def to_code(config): + return await libretiny.component_to_code(config) + + +@pins.PIN_SCHEMA_REGISTRY.register("ln882x", PIN_SCHEMA) +async def pin_to_code(config): + return await libretiny.gpio.component_pin_to_code(config) diff --git a/esphome/components/ln882x/boards.py b/esphome/components/ln882x/boards.py new file mode 100644 index 0000000000..43f25994a7 --- /dev/null +++ b/esphome/components/ln882x/boards.py @@ -0,0 +1,285 @@ +# This file was auto-generated by libretiny/generate_components.py +# Do not modify its contents. + +from esphome.components.libretiny.const import FAMILY_LN882H + +LN882X_BOARDS = { + "wl2s": { + "name": "WL2S Wi-Fi/BLE Module", + "family": FAMILY_LN882H, + }, + "ln-02": { + "name": "LN-02 Wi-Fi/BLE Module", + "family": FAMILY_LN882H, + }, + "generic-ln882hki": { + "name": "Generic - LN882HKI", + "family": FAMILY_LN882H, + }, +} + +LN882X_BOARD_PINS = { + "wl2s": { + "WIRE0_SCL_0": 7, + "WIRE0_SCL_1": 12, + "WIRE0_SCL_2": 3, + "WIRE0_SCL_3": 10, + "WIRE0_SCL_4": 2, + "WIRE0_SCL_5": 0, + "WIRE0_SCL_6": 19, + "WIRE0_SCL_7": 11, + "WIRE0_SCL_8": 9, + "WIRE0_SCL_9": 24, + "WIRE0_SCL_10": 25, + "WIRE0_SCL_11": 5, + "WIRE0_SCL_12": 1, + "WIRE0_SDA_0": 7, + "WIRE0_SDA_1": 12, + "WIRE0_SDA_2": 3, + "WIRE0_SDA_3": 10, + "WIRE0_SDA_4": 2, + "WIRE0_SDA_5": 0, + "WIRE0_SDA_6": 19, + "WIRE0_SDA_7": 11, + "WIRE0_SDA_8": 9, + "WIRE0_SDA_9": 24, + "WIRE0_SDA_10": 25, + "WIRE0_SDA_11": 5, + "WIRE0_SDA_12": 1, + "SERIAL0_RX": 3, + "SERIAL0_TX": 2, + "SERIAL1_RX": 24, + "SERIAL1_TX": 25, + "ADC2": 0, + "ADC3": 1, + "ADC5": 19, + "PA00": 0, + "PA0": 0, + "PA01": 1, + "PA1": 1, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA05": 5, + "PA5": 5, + "PA07": 7, + "PA7": 7, + "PA09": 9, + "PA9": 9, + "PA10": 10, + "PA11": 11, + "PA12": 12, + "PB03": 19, + "PB3": 19, + "PB08": 24, + "PB8": 24, + "PB09": 25, + "PB9": 25, + "RX0": 3, + "RX1": 24, + "SCL0": 1, + "SDA0": 1, + "TX0": 2, + "TX1": 25, + "D0": 7, + "D1": 12, + "D2": 3, + "D3": 10, + "D4": 2, + "D5": 0, + "D6": 19, + "D7": 11, + "D8": 9, + "D9": 24, + "D10": 25, + "D11": 5, + "D12": 1, + "A0": 0, + "A1": 19, + "A2": 1, + }, + "ln-02": { + "WIRE0_SCL_0": 11, + "WIRE0_SCL_1": 19, + "WIRE0_SCL_2": 3, + "WIRE0_SCL_3": 24, + "WIRE0_SCL_4": 2, + "WIRE0_SCL_5": 25, + "WIRE0_SCL_6": 1, + "WIRE0_SCL_7": 0, + "WIRE0_SCL_8": 9, + "WIRE0_SDA_0": 11, + "WIRE0_SDA_1": 19, + "WIRE0_SDA_2": 3, + "WIRE0_SDA_3": 24, + "WIRE0_SDA_4": 2, + "WIRE0_SDA_5": 25, + "WIRE0_SDA_6": 1, + "WIRE0_SDA_7": 0, + "WIRE0_SDA_8": 9, + "SERIAL0_RX": 3, + "SERIAL0_TX": 2, + "SERIAL1_RX": 24, + "SERIAL1_TX": 25, + "ADC2": 0, + "ADC3": 1, + "ADC5": 19, + "PA00": 0, + "PA0": 0, + "PA01": 1, + "PA1": 1, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA09": 9, + "PA9": 9, + "PA11": 11, + "PB03": 19, + "PB3": 19, + "PB08": 24, + "PB8": 24, + "PB09": 25, + "PB9": 25, + "RX0": 3, + "RX1": 24, + "SCL0": 9, + "SDA0": 9, + "TX0": 2, + "TX1": 25, + "D0": 11, + "D1": 19, + "D2": 3, + "D3": 24, + "D4": 2, + "D5": 25, + "D6": 1, + "D7": 0, + "D8": 9, + "A0": 19, + "A1": 1, + "A2": 0, + }, + "generic-ln882hki": { + "WIRE0_SCL_0": 0, + "WIRE0_SCL_1": 1, + "WIRE0_SCL_2": 2, + "WIRE0_SCL_3": 3, + "WIRE0_SCL_4": 4, + "WIRE0_SCL_5": 5, + "WIRE0_SCL_6": 6, + "WIRE0_SCL_7": 7, + "WIRE0_SCL_8": 8, + "WIRE0_SCL_9": 9, + "WIRE0_SCL_10": 10, + "WIRE0_SCL_11": 11, + "WIRE0_SCL_12": 12, + "WIRE0_SCL_13": 19, + "WIRE0_SCL_14": 20, + "WIRE0_SCL_15": 21, + "WIRE0_SCL_16": 22, + "WIRE0_SCL_17": 23, + "WIRE0_SCL_18": 24, + "WIRE0_SCL_19": 25, + "WIRE0_SDA_0": 0, + "WIRE0_SDA_1": 1, + "WIRE0_SDA_2": 2, + "WIRE0_SDA_3": 3, + "WIRE0_SDA_4": 4, + "WIRE0_SDA_5": 5, + "WIRE0_SDA_6": 6, + "WIRE0_SDA_7": 7, + "WIRE0_SDA_8": 8, + "WIRE0_SDA_9": 9, + "WIRE0_SDA_10": 10, + "WIRE0_SDA_11": 11, + "WIRE0_SDA_12": 12, + "WIRE0_SDA_13": 19, + "WIRE0_SDA_14": 20, + "WIRE0_SDA_15": 21, + "WIRE0_SDA_16": 22, + "WIRE0_SDA_17": 23, + "WIRE0_SDA_18": 24, + "WIRE0_SDA_19": 25, + "SERIAL0_RX": 3, + "SERIAL0_TX": 2, + "SERIAL1_RX": 24, + "SERIAL1_TX": 25, + "ADC2": 0, + "ADC3": 1, + "ADC4": 4, + "ADC5": 19, + "ADC6": 20, + "ADC7": 21, + "PA00": 0, + "PA0": 0, + "PA01": 1, + "PA1": 1, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA04": 4, + "PA4": 4, + "PA05": 5, + "PA5": 5, + "PA06": 6, + "PA6": 6, + "PA07": 7, + "PA7": 7, + "PA08": 8, + "PA8": 8, + "PA09": 9, + "PA9": 9, + "PA10": 10, + "PA11": 11, + "PA12": 12, + "PB03": 19, + "PB3": 19, + "PB04": 20, + "PB4": 20, + "PB05": 21, + "PB5": 21, + "PB06": 22, + "PB6": 22, + "PB07": 23, + "PB7": 23, + "PB08": 24, + "PB8": 24, + "PB09": 25, + "PB9": 25, + "RX0": 3, + "RX1": 24, + "TX0": 2, + "TX1": 25, + "D0": 0, + "D1": 1, + "D2": 2, + "D3": 3, + "D4": 4, + "D5": 5, + "D6": 6, + "D7": 7, + "D8": 8, + "D9": 9, + "D10": 10, + "D11": 11, + "D12": 12, + "D13": 19, + "D14": 20, + "D15": 21, + "D16": 22, + "D17": 23, + "D18": 24, + "D19": 25, + "A2": 0, + "A3": 1, + "A4": 4, + "A5": 19, + "A6": 20, + "A7": 21, + }, +} + +BOARDS = LN882X_BOARDS diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index af62d8a73f..3d4907aa6e 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -16,7 +16,11 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S3, ) from esphome.components.libretiny import get_libretiny_component, get_libretiny_family -from esphome.components.libretiny.const import COMPONENT_BK72XX, COMPONENT_RTL87XX +from esphome.components.libretiny.const import ( + COMPONENT_BK72XX, + COMPONENT_LN882X, + COMPONENT_RTL87XX, +) import esphome.config_validation as cv from esphome.const import ( CONF_ARGS, @@ -35,6 +39,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_LN882X, PLATFORM_RP2040, PLATFORM_RTL87XX, ) @@ -100,6 +105,7 @@ UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1] UART_SELECTION_LIBRETINY = { COMPONENT_BK72XX: [DEFAULT, UART1, UART2], + COMPONENT_LN882X: [DEFAULT, UART0, UART1, UART2], COMPONENT_RTL87XX: [DEFAULT, UART0, UART1, UART2], } @@ -217,6 +223,7 @@ CONFIG_SCHEMA = cv.All( esp32_p4_idf=USB_SERIAL_JTAG, rp2040=USB_CDC, bk72xx=DEFAULT, + ln882x=DEFAULT, rtl87xx=DEFAULT, ): cv.All( cv.only_on( @@ -225,6 +232,7 @@ CONFIG_SCHEMA = cv.All( PLATFORM_ESP32, PLATFORM_RP2040, PLATFORM_BK72XX, + PLATFORM_LN882X, PLATFORM_RTL87XX, ] ), diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 321cfc93ff..5de7d8c9c4 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -94,6 +94,7 @@ CONFIG_SCHEMA = remote_base.validate_triggers( esp32="10000b", esp8266="1000b", bk72xx="1000b", + ln882x="1000b", rtl87xx="1000b", ): cv.validate_bytes, cv.Optional(CONF_FILTER, default="50us"): cv.All( diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index 6f883d5bed..1c8ee402ad 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -7,6 +7,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_LN882X, PLATFORM_RP2040, PLATFORM_RTL87XX, ) @@ -33,6 +34,7 @@ CONFIG_SCHEMA = cv.All( PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_BK72XX, + PLATFORM_LN882X, PLATFORM_RTL87XX, ] ), diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 667e30df4b..26031a8da5 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -16,6 +16,7 @@ CONFIG_SCHEMA = cv.Schema( esp32=IMPLEMENTATION_BSD_SOCKETS, rp2040=IMPLEMENTATION_LWIP_TCP, bk72xx=IMPLEMENTATION_LWIP_SOCKETS, + ln882x=IMPLEMENTATION_LWIP_SOCKETS, rtl87xx=IMPLEMENTATION_LWIP_SOCKETS, host=IMPLEMENTATION_BSD_SOCKETS, ): cv.one_of( diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 8ff7ce1d16..f2c1824028 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -28,6 +28,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_LN882X, PLATFORM_RTL87XX, ) from esphome.core import CORE, coroutine_with_priority @@ -180,6 +181,7 @@ CONFIG_SCHEMA = cv.All( esp32_arduino=True, esp32_idf=False, bk72xx=True, + ln882x=True, rtl87xx=True, ): cv.boolean, cv.Optional(CONF_LOG, default=True): cv.boolean, @@ -187,7 +189,15 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group), } ).extend(cv.COMPONENT_SCHEMA), - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]), + cv.only_on( + [ + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_BK72XX, + PLATFORM_LN882X, + PLATFORM_RTL87XX, + ] + ), default_url, validate_local, validate_ota, diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 582b826de0..e8ae9b1b4e 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -309,6 +309,7 @@ CONFIG_SCHEMA = cv.All( rp2040="light", bk72xx="none", rtl87xx="none", + ln882x="light", ): cv.enum(WIFI_POWER_SAVE_MODES, upper=True), cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, diff --git a/esphome/const.py b/esphome/const.py index ed6390d8c3..b167935d12 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -12,6 +12,7 @@ PLATFORM_ESP32 = "esp32" PLATFORM_ESP8266 = "esp8266" PLATFORM_HOST = "host" PLATFORM_LIBRETINY_OLDSTYLE = "libretiny" +PLATFORM_LN882X = "ln882x" PLATFORM_RP2040 = "rp2040" PLATFORM_RTL87XX = "rtl87xx" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 368e2affe9..e33bbcf726 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -20,6 +20,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_HOST, + PLATFORM_LN882X, PLATFORM_RP2040, PLATFORM_RTL87XX, ) @@ -661,9 +662,13 @@ class EsphomeCore: def is_rtl87xx(self): return self.target_platform == PLATFORM_RTL87XX + @property + def is_ln882x(self): + return self.target_platform == PLATFORM_LN882X + @property def is_libretiny(self): - return self.is_bk72xx or self.is_rtl87xx + return self.is_bk72xx or self.is_rtl87xx or self.is_ln882x @property def is_host(self): diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 529a0815b8..480285b6c1 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -639,7 +639,11 @@ class DownloadListRequestHandler(BaseHandler): if platform.upper() in ESP32_VARIANTS: platform = "esp32" - elif platform in (const.PLATFORM_RTL87XX, const.PLATFORM_BK72XX): + elif platform in ( + const.PLATFORM_RTL87XX, + const.PLATFORM_BK72XX, + const.PLATFORM_LN882X, + ): platform = "libretiny" try: @@ -837,6 +841,10 @@ class BoardsRequestHandler(BaseHandler): from esphome.components.bk72xx.boards import BOARDS as BK72XX_BOARDS boards = BK72XX_BOARDS + elif platform == const.PLATFORM_LN882X: + from esphome.components.ln882x.boards import BOARDS as LN882X_BOARDS + + boards = LN882X_BOARDS elif platform == const.PLATFORM_RTL87XX: from esphome.components.rtl87xx.boards import BOARDS as RTL87XX_BOARDS diff --git a/esphome/wizard.py b/esphome/wizard.py index 7b4d87be63..1826487aa4 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -83,6 +83,11 @@ bk72xx: board: {board} """ +LN882X_CONFIG = """ +ln882x: + board: {board} +""" + RTL87XX_CONFIG = """ rtl87xx: board: {board} @@ -93,6 +98,7 @@ HARDWARE_BASE_CONFIGS = { "ESP32": ESP32_CONFIG, "RP2040": RP2040_CONFIG, "BK72XX": BK72XX_CONFIG, + "LN882X": LN882X_CONFIG, "RTL87XX": RTL87XX_CONFIG, } @@ -157,7 +163,7 @@ def wizard_file(**kwargs): """ # pylint: disable=consider-using-f-string - if kwargs["platform"] in ["ESP8266", "ESP32", "BK72XX", "RTL87XX"]: + if kwargs["platform"] in ["ESP8266", "ESP32", "BK72XX", "LN882X", "RTL87XX"]: config += """ # Enable fallback hotspot (captive portal) in case wifi connection fails ap: @@ -181,6 +187,7 @@ def wizard_write(path, **kwargs): from esphome.components.bk72xx import boards as bk72xx_boards from esphome.components.esp32 import boards as esp32_boards from esphome.components.esp8266 import boards as esp8266_boards + from esphome.components.ln882x import boards as ln882x_boards from esphome.components.rp2040 import boards as rp2040_boards from esphome.components.rtl87xx import boards as rtl87xx_boards @@ -200,6 +207,8 @@ def wizard_write(path, **kwargs): platform = "RP2040" elif board in bk72xx_boards.BOARDS: platform = "BK72XX" + elif board in ln882x_boards.BOARDS: + platform = "LN882X" elif board in rtl87xx_boards.BOARDS: platform = "RTL87XX" else: @@ -253,6 +262,7 @@ def wizard(path): from esphome.components.bk72xx import boards as bk72xx_boards from esphome.components.esp32 import boards as esp32_boards from esphome.components.esp8266 import boards as esp8266_boards + from esphome.components.ln882x import boards as ln882x_boards from esphome.components.rp2040 import boards as rp2040_boards from esphome.components.rtl87xx import boards as rtl87xx_boards @@ -325,7 +335,7 @@ def wizard(path): "firmwares for it." ) - wizard_platforms = ["ESP32", "ESP8266", "BK72XX", "RTL87XX", "RP2040"] + wizard_platforms = ["ESP32", "ESP8266", "BK72XX", "LN882X", "RTL87XX", "RP2040"] safe_print( "Please choose one of the supported microcontrollers " "(Use ESP8266 for Sonoff devices)." @@ -361,7 +371,7 @@ def wizard(path): board_link = ( "https://www.raspberrypi.com/documentation/microcontrollers/rp2040.html" ) - elif platform in ["BK72XX", "RTL87XX"]: + elif platform in ["BK72XX", "LN882X", "RTL87XX"]: board_link = "https://docs.libretiny.eu/docs/status/supported/" else: raise NotImplementedError("Unknown platform!") @@ -384,6 +394,9 @@ def wizard(path): elif platform == "BK72XX": safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "cb2s")}".') boards_list = bk72xx_boards.BOARDS.items() + elif platform == "LN882X": + safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "wl2s")}".') + boards_list = ln882x_boards.BOARDS.items() elif platform == "RTL87XX": safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "wr3")}".') boards_list = rtl87xx_boards.BOARDS.items() diff --git a/platformio.ini b/platformio.ini index be9d7587c2..79e22f90b0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -4,7 +4,7 @@ ; It's *not* used during runtime. [platformio] -default_envs = esp8266-arduino, esp32-arduino, esp32-idf, bk72xx-arduino +default_envs = esp8266-arduino, esp32-arduino, esp32-idf, bk72xx-arduino, ln882h-arduino ; Ideally, we want src_dir to be the root directory of the repository, to mimic the runtime build ; environment as best as possible. Unfortunately, the ESP-IDF toolchain really doesn't like this ; being the root directory. Instead, set esphome/ as the source directory, all our sources are in @@ -530,6 +530,17 @@ build_flags = build_unflags = ${common.build_unflags} +[env:ln882h-arduino] +extends = common:libretiny-arduino +board = generic-ln882hki +build_flags = + ${common:libretiny-arduino.build_flags} + ${flags:runtime.build_flags} + -DUSE_LN882X + -DUSE_LIBRETINY_VARIANT_LN882H +build_unflags = + ${common.build_unflags} + [env:rtl87xxb-arduino] extends = common:libretiny-arduino board = generic-rtl8710bn-2mb-788k diff --git a/tests/components/adc/test.ln882x-ard.yaml b/tests/components/adc/test.ln882x-ard.yaml new file mode 100644 index 0000000000..92c76ca9b3 --- /dev/null +++ b/tests/components/adc/test.ln882x-ard.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: adc + pin: PA0 + name: Basic ADC Test diff --git a/tests/components/binary_sensor/test.ln882x-ard.yaml b/tests/components/binary_sensor/test.ln882x-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/binary_sensor/test.ln882x-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/debug/test.ln882x-ard.yaml b/tests/components/debug/test.ln882x-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.ln882x-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/homeassistant/test.ln882x-ard.yaml b/tests/components/homeassistant/test.ln882x-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/homeassistant/test.ln882x-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/script/test.ln882x-ard.yaml b/tests/components/script/test.ln882x-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/script/test.ln882x-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sntp/test.ln882x-ard.yaml b/tests/components/sntp/test.ln882x-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sntp/test.ln882x-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/switch/test.ln882x-ard.yaml b/tests/components/switch/test.ln882x-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/switch/test.ln882x-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/syslog/test.ln882x-ard.yaml b/tests/components/syslog/test.ln882x-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/syslog/test.ln882x-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/template/test.ln882x-ard.yaml b/tests/components/template/test.ln882x-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/template/test.ln882x-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/test_build_components/build_components_base.ln882x-ard.yaml b/tests/test_build_components/build_components_base.ln882x-ard.yaml new file mode 100644 index 0000000000..80fc6690f9 --- /dev/null +++ b/tests/test_build_components/build_components_base.ln882x-ard.yaml @@ -0,0 +1,15 @@ +esphome: + name: componenttestespln882x + friendly_name: $component_name + +ln882x: + board: generic-ln882hki + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_test_file: $component_test_file diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 7a1354589c..2928c5c83a 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -20,6 +20,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_HOST, + PLATFORM_LN882X, PLATFORM_RP2040, PLATFORM_RTL87XX, ) @@ -214,7 +215,8 @@ def hex_int__valid(value): ("arduino", PLATFORM_RP2040, None, "20", "20", "20", "20"), ("arduino", PLATFORM_BK72XX, None, "21", "21", "21", "21"), ("arduino", PLATFORM_RTL87XX, None, "22", "22", "22", "22"), - ("host", PLATFORM_HOST, None, "23", "23", "23", "23"), + ("arduino", PLATFORM_LN882X, None, "23", "23", "23", "23"), + ("host", PLATFORM_HOST, None, "24", "24", "24", "24"), ], ) def test_split_default(framework, platform, variant, full, idf, arduino, simple): @@ -244,7 +246,8 @@ def test_split_default(framework, platform, variant, full, idf, arduino, simple) "rp2040": "20", "bk72xx": "21", "rtl87xx": "22", - "host": "23", + "ln882x": "23", + "host": "24", } idf_mappings = { diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index 6d360740f4..ab20b2abb5 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -8,6 +8,7 @@ import pytest from esphome.components.bk72xx.boards import BK72XX_BOARD_PINS from esphome.components.esp32.boards import ESP32_BOARD_PINS from esphome.components.esp8266.boards import ESP8266_BOARD_PINS +from esphome.components.ln882x.boards import LN882X_BOARD_PINS from esphome.components.rtl87xx.boards import RTL87XX_BOARD_PINS from esphome.core import CORE import esphome.wizard as wz @@ -187,6 +188,27 @@ def test_wizard_write_defaults_platform_from_board_bk72xx( assert "bk72xx:" in generated_config +def test_wizard_write_defaults_platform_from_board_ln882x( + default_config, tmp_path, monkeypatch +): + """ + If the platform is not explicitly set, use "LN882X" if the board is one of LN882X boards + """ + # Given + del default_config["platform"] + default_config["board"] = [*LN882X_BOARD_PINS][0] + + monkeypatch.setattr(wz, "write_file", MagicMock()) + monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path)) + + # When + wz.wizard_write(tmp_path, **default_config) + + # Then + generated_config = wz.write_file.call_args.args[1] + assert "ln882x:" in generated_config + + def test_wizard_write_defaults_platform_from_board_rtl87xx( default_config, tmp_path, monkeypatch ): From 8a9769d4e958e6f9773ed245fa1aa11ea8ca1f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Mork?= Date: Mon, 30 Jun 2025 06:49:38 +0200 Subject: [PATCH 179/293] Support DM9051 SPI ethernet device (#6861) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/ethernet/__init__.py | 10 +++-- .../ethernet/ethernet_component.cpp | 41 +++++++++++++++++-- .../components/ethernet/ethernet_component.h | 1 + tests/components/ethernet/common-dm9051.yaml | 14 +++++++ .../ethernet/test-dm9051.esp32-ard.yaml | 1 + .../ethernet/test-dm9051.esp32-idf.yaml | 1 + 6 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 tests/components/ethernet/common-dm9051.yaml create mode 100644 tests/components/ethernet/test-dm9051.esp32-ard.yaml create mode 100644 tests/components/ethernet/test-dm9051.esp32-idf.yaml diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index cd77ea6053..8eec9510cc 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -66,9 +66,10 @@ ETHERNET_TYPES = { "KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA, "W5500": EthernetType.ETHERNET_TYPE_W5500, "OPENETH": EthernetType.ETHERNET_TYPE_OPENETH, + "DM9051": EthernetType.ETHERNET_TYPE_DM9051, } -SPI_ETHERNET_TYPES = ["W5500"] +SPI_ETHERNET_TYPES = ["W5500", "DM9051"] SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10) emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") @@ -224,6 +225,7 @@ CONFIG_SCHEMA = cv.All( "KSZ8081RNA": RMII_SCHEMA, "W5500": SPI_SCHEMA, "OPENETH": BASE_SCHEMA, + "DM9051": SPI_SCHEMA, }, upper=True, ), @@ -278,7 +280,7 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - if config[CONF_TYPE] == "W5500": + if config[CONF_TYPE] in SPI_ETHERNET_TYPES: cg.add(var.set_clk_pin(config[CONF_CLK_PIN])) cg.add(var.set_miso_pin(config[CONF_MISO_PIN])) cg.add(var.set_mosi_pin(config[CONF_MOSI_PIN])) @@ -296,7 +298,9 @@ async def to_code(config): cg.add_define("USE_ETHERNET_SPI") if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) - add_idf_sdkconfig_option("CONFIG_ETH_SPI_ETHERNET_W5500", True) + add_idf_sdkconfig_option( + f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True + ) elif config[CONF_TYPE] == "OPENETH": cg.add_define("USE_ETHERNET_OPENETH") add_idf_sdkconfig_option("CONFIG_ETH_USE_OPENETH", True) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 180a72ec7e..8739269f4a 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -90,8 +90,8 @@ void EthernetComponent::setup() { #ifdef USE_ETHERNET_SPI // Configure SPI interface and Ethernet driver for specific SPI module spi_device_interface_config_t devcfg = { - .command_bits = 16, // Actually it's the address phase in W5500 SPI frame - .address_bits = 8, // Actually it's the control phase in W5500 SPI frame + .command_bits = 0, + .address_bits = 0, .dummy_bits = 0, .mode = 0, .duty_cycle_pos = 0, @@ -107,22 +107,43 @@ void EthernetComponent::setup() { }; #if ESP_IDF_VERSION_MAJOR >= 5 +#if CONFIG_ETH_SPI_ETHERNET_W5500 eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg); +#endif +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg); +#endif #else spi_device_handle_t spi_handle = nullptr; err = spi_bus_add_device(host, &devcfg, &spi_handle); ESPHL_ERROR_CHECK(err, "SPI bus add device error"); +#if CONFIG_ETH_SPI_ETHERNET_W5500 eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(spi_handle); #endif +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(spi_handle); +#endif +#endif // ESP_IDF_VERSION_MAJOR >= 5 + +#if CONFIG_ETH_SPI_ETHERNET_W5500 w5500_config.int_gpio_num = this->interrupt_pin_; #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT w5500_config.poll_period_ms = this->polling_interval_; #endif +#endif + +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + dm9051_config.int_gpio_num = this->interrupt_pin_; +#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT + dm9051_config.poll_period_ms = this->polling_interval_; +#endif +#endif + phy_config.phy_addr = this->phy_addr_spi_; phy_config.reset_gpio_num = this->reset_pin_; - esp_eth_mac_t *mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); + esp_eth_mac_t *mac = nullptr; #elif defined(USE_ETHERNET_OPENETH) esp_eth_mac_t *mac = esp_eth_mac_new_openeth(&mac_config); #else @@ -187,10 +208,20 @@ void EthernetComponent::setup() { } #endif #ifdef USE_ETHERNET_SPI +#if CONFIG_ETH_SPI_ETHERNET_W5500 case ETHERNET_TYPE_W5500: { + mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); this->phy_ = esp_eth_phy_new_w5500(&phy_config); break; } +#endif +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + case ETHERNET_TYPE_DM9051: { + mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config); + this->phy_ = esp_eth_phy_new_dm9051(&phy_config); + break; + } +#endif #endif default: { this->mark_failed(); @@ -321,6 +352,10 @@ void EthernetComponent::dump_config() { eth_type = "OPENETH"; break; + case ETHERNET_TYPE_DM9051: + eth_type = "DM9051"; + break; + default: eth_type = "Unknown"; break; diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 0f0eff5ded..6cdc113aa8 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -26,6 +26,7 @@ enum EthernetType : uint8_t { ETHERNET_TYPE_KSZ8081RNA, ETHERNET_TYPE_W5500, ETHERNET_TYPE_OPENETH, + ETHERNET_TYPE_DM9051, }; struct ManualIP { diff --git a/tests/components/ethernet/common-dm9051.yaml b/tests/components/ethernet/common-dm9051.yaml new file mode 100644 index 0000000000..c878ca6e59 --- /dev/null +++ b/tests/components/ethernet/common-dm9051.yaml @@ -0,0 +1,14 @@ +ethernet: + type: DM9051 + clk_pin: 19 + mosi_pin: 21 + miso_pin: 23 + cs_pin: 18 + interrupt_pin: 36 + reset_pin: 22 + clock_speed: 10Mhz + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local diff --git a/tests/components/ethernet/test-dm9051.esp32-ard.yaml b/tests/components/ethernet/test-dm9051.esp32-ard.yaml new file mode 100644 index 0000000000..23e3b97740 --- /dev/null +++ b/tests/components/ethernet/test-dm9051.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common-dm9051.yaml diff --git a/tests/components/ethernet/test-dm9051.esp32-idf.yaml b/tests/components/ethernet/test-dm9051.esp32-idf.yaml new file mode 100644 index 0000000000..23e3b97740 --- /dev/null +++ b/tests/components/ethernet/test-dm9051.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-dm9051.yaml From af0bb634c65c4b02110ad753f7b801437b1af6e7 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 30 Jun 2025 00:05:52 -0500 Subject: [PATCH 180/293] [light] Fix transitions with ``lerp`` (#9269) --- esphome/components/light/light_color_values.h | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index ca32b9c571..d8eaa6ae24 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -86,16 +86,16 @@ class LightColorValues { static LightColorValues lerp(const LightColorValues &start, const LightColorValues &end, float completion) { LightColorValues v; v.set_color_mode(end.color_mode_); - v.set_state(esphome::lerp(completion, start.get_state(), end.get_state())); - v.set_brightness(esphome::lerp(completion, start.get_brightness(), end.get_brightness())); - v.set_color_brightness(esphome::lerp(completion, start.get_color_brightness(), end.get_color_brightness())); - v.set_red(esphome::lerp(completion, start.get_red(), end.get_red())); - v.set_green(esphome::lerp(completion, start.get_green(), end.get_green())); - v.set_blue(esphome::lerp(completion, start.get_blue(), end.get_blue())); - v.set_white(esphome::lerp(completion, start.get_white(), end.get_white())); - v.set_color_temperature(esphome::lerp(completion, start.get_color_temperature(), end.get_color_temperature())); - v.set_cold_white(esphome::lerp(completion, start.get_cold_white(), end.get_cold_white())); - v.set_warm_white(esphome::lerp(completion, start.get_warm_white(), end.get_warm_white())); + v.set_state(std::lerp(start.get_state(), end.get_state(), completion)); + v.set_brightness(std::lerp(start.get_brightness(), end.get_brightness(), completion)); + v.set_color_brightness(std::lerp(start.get_color_brightness(), end.get_color_brightness(), completion)); + v.set_red(std::lerp(start.get_red(), end.get_red(), completion)); + v.set_green(std::lerp(start.get_green(), end.get_green(), completion)); + v.set_blue(std::lerp(start.get_blue(), end.get_blue(), completion)); + v.set_white(std::lerp(start.get_white(), end.get_white(), completion)); + v.set_color_temperature(std::lerp(start.get_color_temperature(), end.get_color_temperature(), completion)); + v.set_cold_white(std::lerp(start.get_cold_white(), end.get_cold_white(), completion)); + v.set_warm_white(std::lerp(start.get_warm_white(), end.get_warm_white(), completion)); return v; } From 3e553f517b2f5683182d0a447db22130b5d6fc69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Poczkodi?= Date: Mon, 30 Jun 2025 07:12:44 +0200 Subject: [PATCH 181/293] [remote_base] Fix dumper base class and enable schema extension (#9218) --- esphome/components/remote_base/__init__.py | 12 ++++++---- .../components/remote_base/remote_base.cpp | 24 +++++++++++++++++++ esphome/components/remote_base/remote_base.h | 3 +++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 836b98104b..fc824ef704 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -57,7 +57,7 @@ RemoteReceiverBinarySensorBase = ns.class_( RemoteReceiverTrigger = ns.class_( "RemoteReceiverTrigger", automation.Trigger, RemoteReceiverListener ) -RemoteTransmitterDumper = ns.class_("RemoteTransmitterDumper") +RemoteReceiverDumperBase = ns.class_("RemoteReceiverDumperBase") RemoteTransmittable = ns.class_("RemoteTransmittable") RemoteTransmitterActionBase = ns.class_( "RemoteTransmitterActionBase", RemoteTransmittable, automation.Action @@ -126,8 +126,10 @@ def register_trigger(name, type, data_type): return decorator -def register_dumper(name, type): - registerer = DUMPER_REGISTRY.register(name, type, {}) +def register_dumper(name, type, schema=None): + if schema is None: + schema = {} + registerer = DUMPER_REGISTRY.register(name, type, schema) def decorator(func): async def new_func(config, dumper_id): @@ -189,7 +191,7 @@ def declare_protocol(name): binary_sensor_ = ns.class_(f"{name}BinarySensor", RemoteReceiverBinarySensorBase) trigger = ns.class_(f"{name}Trigger", RemoteReceiverTrigger) action = ns.class_(f"{name}Action", RemoteTransmitterActionBase) - dumper = ns.class_(f"{name}Dumper", RemoteTransmitterDumper) + dumper = ns.class_(f"{name}Dumper", RemoteReceiverDumperBase) return data, binary_sensor_, trigger, action, dumper @@ -1405,7 +1407,7 @@ rc_switch_protocols = ns.RC_SWITCH_PROTOCOLS RCSwitchData = ns.struct("RCSwitchData") RCSwitchBase = ns.class_("RCSwitchBase") RCSwitchTrigger = ns.class_("RCSwitchTrigger", RemoteReceiverTrigger) -RCSwitchDumper = ns.class_("RCSwitchDumper", RemoteTransmitterDumper) +RCSwitchDumper = ns.class_("RCSwitchDumper", RemoteReceiverDumperBase) RCSwitchRawAction = ns.class_("RCSwitchRawAction", RemoteTransmitterActionBase) RCSwitchTypeAAction = ns.class_("RCSwitchTypeAAction", RemoteTransmitterActionBase) RCSwitchTypeBAction = ns.class_("RCSwitchTypeBAction", RemoteTransmitterActionBase) diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 987286b345..34aba236b3 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -19,6 +19,22 @@ bool RemoteReceiveData::peek_mark(uint32_t length, uint32_t offset) const { return value >= 0 && lo <= value && value <= hi; } +bool RemoteReceiveData::peek_mark_at_least(uint32_t length, uint32_t offset) const { + if (!this->is_valid(offset)) + return false; + const int32_t value = this->peek(offset); + const int32_t lo = this->lower_bound_(length); + return value >= 0 && lo <= value; +} + +bool RemoteReceiveData::peek_mark_at_most(uint32_t length, uint32_t offset) const { + if (!this->is_valid(offset)) + return false; + const int32_t value = this->peek(offset); + const int32_t hi = this->upper_bound_(length); + return value >= 0 && value <= hi; +} + bool RemoteReceiveData::peek_space(uint32_t length, uint32_t offset) const { if (!this->is_valid(offset)) return false; @@ -36,6 +52,14 @@ bool RemoteReceiveData::peek_space_at_least(uint32_t length, uint32_t offset) co return value <= 0 && lo <= -value; } +bool RemoteReceiveData::peek_space_at_most(uint32_t length, uint32_t offset) const { + if (!this->is_valid(offset)) + return false; + const int32_t value = this->peek(offset); + const int32_t hi = this->upper_bound_(length); + return value <= 0 && -value <= hi; +} + bool RemoteReceiveData::expect_mark(uint32_t length) { if (!this->peek_mark(length)) return false; diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index a18dd0ed7e..b740ba8085 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -53,8 +53,11 @@ class RemoteReceiveData { bool is_valid(uint32_t offset = 0) const { return this->index_ + offset < this->data_.size(); } int32_t peek(uint32_t offset = 0) const { return this->data_[this->index_ + offset]; } bool peek_mark(uint32_t length, uint32_t offset = 0) const; + bool peek_mark_at_least(uint32_t length, uint32_t offset = 0) const; + bool peek_mark_at_most(uint32_t length, uint32_t offset = 0) const; bool peek_space(uint32_t length, uint32_t offset = 0) const; bool peek_space_at_least(uint32_t length, uint32_t offset = 0) const; + bool peek_space_at_most(uint32_t length, uint32_t offset = 0) const; bool peek_item(uint32_t mark, uint32_t space, uint32_t offset = 0) const { return this->peek_space(space, offset + 1) && this->peek_mark(mark, offset); } From 3930609d8b25de44f348ed8b4465b39c93d1b18b Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 30 Jun 2025 01:05:59 -0500 Subject: [PATCH 182/293] [ld2420] Move consts to cpp file, optimize memory use (#9216) --- esphome/components/ld2420/ld2420.cpp | 191 +++++++++++++----- esphome/components/ld2420/ld2420.h | 168 +++++---------- .../components/ld2420/sensor/ld2420_sensor.h | 2 +- 3 files changed, 194 insertions(+), 167 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 62f1685598..8a7d7de23b 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -63,38 +63,105 @@ namespace ld2420 { static const char *const TAG = "ld2420"; -float LD2420Component::get_setup_priority() const { return setup_priority::BUS; } +// Local const's +static const uint16_t REFRESH_RATE_MS = 1000; -void LD2420Component::dump_config() { - ESP_LOGCONFIG(TAG, - "LD2420:\n" - " Firmware version: %7s", - this->ld2420_firmware_ver_); -#ifdef USE_NUMBER - ESP_LOGCONFIG(TAG, "Number:"); - LOG_NUMBER(TAG, " Gate Timeout:", this->gate_timeout_number_); - LOG_NUMBER(TAG, " Gate Max Distance:", this->max_gate_distance_number_); - LOG_NUMBER(TAG, " Gate Min Distance:", this->min_gate_distance_number_); - LOG_NUMBER(TAG, " Gate Select:", this->gate_select_number_); - for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { - LOG_NUMBER(TAG, " Gate Move Threshold:", this->gate_move_threshold_numbers_[gate]); - LOG_NUMBER(TAG, " Gate Still Threshold::", this->gate_still_threshold_numbers_[gate]); - } -#endif -#ifdef USE_BUTTON - LOG_BUTTON(TAG, " Apply Config:", this->apply_config_button_); - LOG_BUTTON(TAG, " Revert Edits:", this->revert_config_button_); - LOG_BUTTON(TAG, " Factory Reset:", this->factory_reset_button_); - LOG_BUTTON(TAG, " Restart Module:", this->restart_module_button_); -#endif - ESP_LOGCONFIG(TAG, "Select:"); - LOG_SELECT(TAG, " Operating Mode", this->operating_selector_); - if (LD2420Component::get_firmware_int(this->ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { - ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->ld2420_firmware_ver_); +// Command sets +static const uint16_t CMD_DISABLE_CONF = 0x00FE; +static const uint16_t CMD_ENABLE_CONF = 0x00FF; +static const uint16_t CMD_PARM_HIGH_TRESH = 0x0012; +static const uint16_t CMD_PARM_LOW_TRESH = 0x0021; +static const uint16_t CMD_PROTOCOL_VER = 0x0002; +static const uint16_t CMD_READ_ABD_PARAM = 0x0008; +static const uint16_t CMD_READ_REG_ADDR = 0x0020; +static const uint16_t CMD_READ_REGISTER = 0x0002; +static const uint16_t CMD_READ_SERIAL_NUM = 0x0011; +static const uint16_t CMD_READ_SYS_PARAM = 0x0013; +static const uint16_t CMD_READ_VERSION = 0x0000; +static const uint16_t CMD_RESTART = 0x0068; +static const uint16_t CMD_SYSTEM_MODE = 0x0000; +static const uint16_t CMD_SYSTEM_MODE_GR = 0x0003; +static const uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; +static const uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; +static const uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; +static const uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; +static const uint16_t CMD_SYSTEM_MODE_VS = 0x0002; +static const uint16_t CMD_WRITE_ABD_PARAM = 0x0007; +static const uint16_t CMD_WRITE_REGISTER = 0x0001; +static const uint16_t CMD_WRITE_SYS_PARAM = 0x0012; + +static const uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; +static const uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; +static const uint8_t CMD_MAX_BYTES = 0x64; +static const uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; + +static const uint8_t LD2420_ERROR_NONE = 0x00; +static const uint8_t LD2420_ERROR_TIMEOUT = 0x02; +static const uint8_t LD2420_ERROR_UNKNOWN = 0x01; + +// Register address values +static const uint16_t CMD_MIN_GATE_REG = 0x0000; +static const uint16_t CMD_MAX_GATE_REG = 0x0001; +static const uint16_t CMD_TIMEOUT_REG = 0x0004; +static const uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, + 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, + 0x001C, 0x001D, 0x001E, 0x001F}; +static const uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, + 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, + 0x002C, 0x002D, 0x002E, 0x002F}; +static const uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, + 250, 250, 250, 250, 250, 250, 250, 250}; +static const uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, + 150, 100, 100, 100, 100, 100, 100, 100}; +static const uint16_t FACTORY_TIMEOUT = 120; +static const uint16_t FACTORY_MIN_GATE = 1; +static const uint16_t FACTORY_MAX_GATE = 12; + +// COMMAND_BYTE Header & Footer +static const uint32_t CMD_FRAME_FOOTER = 0x01020304; +static const uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; +static const uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; +static const uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; +static const uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; +static const uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; +static const int CALIBRATE_VERSION_MIN = 154; +static const uint8_t CMD_FRAME_COMMAND = 6; +static const uint8_t CMD_FRAME_DATA_LENGTH = 4; +static const uint8_t CMD_FRAME_STATUS = 7; +static const uint8_t CMD_ERROR_WORD = 8; +static const uint8_t ENERGY_SENSOR_START = 9; +static const uint8_t CALIBRATE_REPORT_INTERVAL = 4; +static const std::string OP_NORMAL_MODE_STRING = "Normal"; +static const std::string OP_SIMPLE_MODE_STRING = "Simple"; + +// Memory-efficient lookup tables +struct StringToUint8 { + const char *str; + uint8_t value; +}; + +static constexpr StringToUint8 OP_MODE_BY_STR[] = { + {"Normal", OP_NORMAL_MODE}, + {"Calibrate", OP_CALIBRATE_MODE}, + {"Simple", OP_SIMPLE_MODE}, +}; + +static constexpr const char *ERR_MESSAGE[] = { + "None", + "Unknown", + "Timeout", +}; + +// Helper function for lookups +template uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { + for (const auto &entry : arr) { + if (str == entry.str) + return entry.value; } + return 0xFF; // Not found } -uint8_t LD2420Component::calc_checksum(void *data, size_t size) { +static uint8_t calc_checksum(void *data, size_t size) { uint8_t checksum = 0; uint8_t *data_bytes = (uint8_t *) data; for (size_t i = 0; i < size; i++) { @@ -103,7 +170,7 @@ uint8_t LD2420Component::calc_checksum(void *data, size_t size) { return checksum; } -int LD2420Component::get_firmware_int(const char *version_string) { +static int get_firmware_int(const char *version_string) { std::string version_str = version_string; if (version_str[0] == 'v') { version_str = version_str.substr(1); @@ -113,6 +180,37 @@ int LD2420Component::get_firmware_int(const char *version_string) { return version_integer; } +float LD2420Component::get_setup_priority() const { return setup_priority::BUS; } + +void LD2420Component::dump_config() { + ESP_LOGCONFIG(TAG, + "LD2420:\n" + " Firmware version: %7s", + this->firmware_ver_); +#ifdef USE_NUMBER + ESP_LOGCONFIG(TAG, "Number:"); + LOG_NUMBER(" ", "Gate Timeout:", this->gate_timeout_number_); + LOG_NUMBER(" ", "Gate Max Distance:", this->max_gate_distance_number_); + LOG_NUMBER(" ", "Gate Min Distance:", this->min_gate_distance_number_); + LOG_NUMBER(" ", "Gate Select:", this->gate_select_number_); + for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { + LOG_NUMBER(" ", "Gate Move Threshold:", this->gate_move_threshold_numbers_[gate]); + LOG_NUMBER(" ", "Gate Still Threshold::", this->gate_still_threshold_numbers_[gate]); + } +#endif +#ifdef USE_BUTTON + LOG_BUTTON(" ", "Apply Config:", this->apply_config_button_); + LOG_BUTTON(" ", "Revert Edits:", this->revert_config_button_); + LOG_BUTTON(" ", "Factory Reset:", this->factory_reset_button_); + LOG_BUTTON(" ", "Restart Module:", this->restart_module_button_); +#endif + ESP_LOGCONFIG(TAG, "Select:"); + LOG_SELECT(" ", "Operating Mode", this->operating_selector_); + if (ld2420::get_firmware_int(this->firmware_ver_) < CALIBRATE_VERSION_MIN) { + ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->firmware_ver_); + } +} + void LD2420Component::setup() { ESP_LOGCONFIG(TAG, "Running setup"); if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { @@ -125,24 +223,24 @@ void LD2420Component::setup() { this->init_gate_config_numbers(); #endif this->get_firmware_version_(); - const char *pfw = this->ld2420_firmware_ver_; + const char *pfw = this->firmware_ver_; std::string fw_str(pfw); for (auto &listener : this->listeners_) { listener->on_fw_version(fw_str); } - for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { delay_microseconds_safe(125); this->get_gate_threshold_(gate); } memcpy(&this->new_config, &this->current_config, sizeof(this->current_config)); - if (LD2420Component::get_firmware_int(this->ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { + if (ld2420::get_firmware_int(this->firmware_ver_) < CALIBRATE_VERSION_MIN) { this->set_operating_mode(OP_SIMPLE_MODE_STRING); this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING); this->set_mode_(CMD_SYSTEM_MODE_SIMPLE); - ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->ld2420_firmware_ver_); + ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->firmware_ver_); } else { this->set_mode_(CMD_SYSTEM_MODE_ENERGY); this->operating_selector_->publish_state(OP_NORMAL_MODE_STRING); @@ -167,7 +265,7 @@ void LD2420Component::apply_config_action() { return; } this->set_min_max_distances_timeout(this->new_config.max_gate, this->new_config.min_gate, this->new_config.timeout); - for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { delay_microseconds_safe(125); this->set_gate_threshold(gate); } @@ -193,7 +291,7 @@ void LD2420Component::factory_reset_action() { this->min_gate_distance_number_->state = FACTORY_MIN_GATE; this->max_gate_distance_number_->state = FACTORY_MAX_GATE; #endif - for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { this->new_config.move_thresh[gate] = FACTORY_MOVE_THRESH[gate]; this->new_config.still_thresh[gate] = FACTORY_STILL_THRESH[gate]; delay_microseconds_safe(125); @@ -241,7 +339,7 @@ void LD2420Component::loop() { } void LD2420Component::update_radar_data(uint16_t const *gate_energy, uint8_t sample_number) { - for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; ++gate) { + for (uint8_t gate = 0; gate < TOTAL_GATES; ++gate) { this->radar_data[gate][sample_number] = gate_energy[gate]; } this->total_sample_number_counter++; @@ -251,7 +349,7 @@ void LD2420Component::auto_calibrate_sensitivity() { // Calculate average and peak values for each gate const float move_factor = gate_move_sensitivity_factor + 1; const float still_factor = (gate_still_sensitivity_factor / 2) + 1; - for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; ++gate) { + for (uint8_t gate = 0; gate < TOTAL_GATES; ++gate) { uint32_t sum = 0; uint16_t peak = 0; @@ -280,7 +378,7 @@ void LD2420Component::auto_calibrate_sensitivity() { } void LD2420Component::report_gate_data() { - for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; ++gate) { + for (uint8_t gate = 0; gate < TOTAL_GATES; ++gate) { // Output results ESP_LOGI(TAG, "Gate: %2d Avg: %5d Peak: %5d", gate, this->gate_avg[gate], this->gate_peak[gate]); } @@ -289,13 +387,13 @@ void LD2420Component::report_gate_data() { void LD2420Component::set_operating_mode(const std::string &state) { // If unsupported firmware ignore mode select - if (LD2420Component::get_firmware_int(ld2420_firmware_ver_) >= CALIBRATE_VERSION_MIN) { - this->current_operating_mode = OP_MODE_TO_UINT.at(state); + if (ld2420::get_firmware_int(firmware_ver_) >= CALIBRATE_VERSION_MIN) { + this->current_operating_mode = find_uint8(OP_MODE_BY_STR, state); // Entering Auto Calibrate we need to clear the privoiuos data collection this->operating_selector_->publish_state(state); if (current_operating_mode == OP_CALIBRATE_MODE) { this->set_calibration_(true); - for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { this->gate_avg[gate] = 0; this->gate_peak[gate] = 0; for (uint8_t i = 0; i < CALIBRATE_SAMPLES; i++) { @@ -330,11 +428,12 @@ void LD2420Component::readline_(int rx_data, uint8_t *buffer, int len) { this->set_cmd_active_(false); // Set command state to inactive after responce. this->handle_ack_data_(buffer, pos); pos = 0; - } else if ((buffer[pos - 2] == 0x0D && buffer[pos - 1] == 0x0A) && (get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) { + } else if ((buffer[pos - 2] == 0x0D && buffer[pos - 1] == 0x0A) && + (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) { this->handle_simple_mode_(buffer, pos); pos = 0; } else if ((memcmp(&buffer[pos - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) && - (get_mode_() == CMD_SYSTEM_MODE_ENERGY)) { + (this->get_mode_() == CMD_SYSTEM_MODE_ENERGY)) { this->handle_energy_mode_(buffer, pos); pos = 0; } @@ -483,8 +582,8 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { ESP_LOGV(TAG, "Set system parameter(s): %2X %s", CMD_WRITE_SYS_PARAM, result); break; case (CMD_READ_VERSION): - memcpy(this->ld2420_firmware_ver_, &buffer[12], buffer[10]); - ESP_LOGV(TAG, "Firmware version: %7s %s", this->ld2420_firmware_ver_, result); + memcpy(this->firmware_ver_, &buffer[12], buffer[10]); + ESP_LOGV(TAG, "Firmware version: %7s %s", this->firmware_ver_, result); break; default: break; @@ -753,7 +852,7 @@ void LD2420Component::init_gate_config_numbers() { this->gate_move_sensitivity_factor_number_->publish_state(this->gate_move_sensitivity_factor); if (this->gate_still_sensitivity_factor_number_ != nullptr) this->gate_still_sensitivity_factor_number_->publish_state(this->gate_still_sensitivity_factor); - for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { if (this->gate_still_threshold_numbers_[gate] != nullptr) { this->gate_still_threshold_numbers_[gate]->publish_state( static_cast(this->current_config.still_thresh[gate])); diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 5e011100e6..d574a25c89 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -16,88 +16,18 @@ #ifdef USE_BUTTON #include "esphome/components/button/button.h" #endif -#include -#include namespace esphome { namespace ld2420 { -// Local const's -static const uint16_t REFRESH_RATE_MS = 1000; - -// Command sets -static const uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; -static const uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; -static const uint16_t CMD_DISABLE_CONF = 0x00FE; -static const uint16_t CMD_ENABLE_CONF = 0x00FF; -static const uint8_t CMD_MAX_BYTES = 0x64; -static const uint16_t CMD_PARM_HIGH_TRESH = 0x0012; -static const uint16_t CMD_PARM_LOW_TRESH = 0x0021; -static const uint16_t CMD_PROTOCOL_VER = 0x0002; -static const uint16_t CMD_READ_ABD_PARAM = 0x0008; -static const uint16_t CMD_READ_REG_ADDR = 0x0020; -static const uint16_t CMD_READ_REGISTER = 0x0002; -static const uint16_t CMD_READ_SERIAL_NUM = 0x0011; -static const uint16_t CMD_READ_SYS_PARAM = 0x0013; -static const uint16_t CMD_READ_VERSION = 0x0000; -static const uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; -static const uint16_t CMD_RESTART = 0x0068; -static const uint16_t CMD_SYSTEM_MODE = 0x0000; -static const uint16_t CMD_SYSTEM_MODE_GR = 0x0003; -static const uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; -static const uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; -static const uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; -static const uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; -static const uint16_t CMD_SYSTEM_MODE_VS = 0x0002; -static const uint16_t CMD_WRITE_ABD_PARAM = 0x0007; -static const uint16_t CMD_WRITE_REGISTER = 0x0001; -static const uint16_t CMD_WRITE_SYS_PARAM = 0x0012; - -static const uint8_t LD2420_ERROR_NONE = 0x00; -static const uint8_t LD2420_ERROR_TIMEOUT = 0x02; -static const uint8_t LD2420_ERROR_UNKNOWN = 0x01; -static const uint8_t LD2420_TOTAL_GATES = 16; +static const uint8_t TOTAL_GATES = 16; static const uint8_t CALIBRATE_SAMPLES = 64; -// Register address values -static const uint16_t CMD_MIN_GATE_REG = 0x0000; -static const uint16_t CMD_MAX_GATE_REG = 0x0001; -static const uint16_t CMD_TIMEOUT_REG = 0x0004; -static const uint16_t CMD_GATE_MOVE_THRESH[LD2420_TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, - 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, - 0x001C, 0x001D, 0x001E, 0x001F}; -static const uint16_t CMD_GATE_STILL_THRESH[LD2420_TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, - 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, - 0x002C, 0x002D, 0x002E, 0x002F}; -static const uint32_t FACTORY_MOVE_THRESH[LD2420_TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, - 250, 250, 250, 250, 250, 250, 250, 250}; -static const uint32_t FACTORY_STILL_THRESH[LD2420_TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, - 150, 100, 100, 100, 100, 100, 100, 100}; -static const uint16_t FACTORY_TIMEOUT = 120; -static const uint16_t FACTORY_MIN_GATE = 1; -static const uint16_t FACTORY_MAX_GATE = 12; - -// COMMAND_BYTE Header & Footer -static const uint8_t CMD_FRAME_COMMAND = 6; -static const uint8_t CMD_FRAME_DATA_LENGTH = 4; -static const uint32_t CMD_FRAME_FOOTER = 0x01020304; -static const uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; -static const uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; -static const uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; -static const uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; -static const uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; -static const uint8_t CMD_FRAME_STATUS = 7; -static const uint8_t CMD_ERROR_WORD = 8; -static const uint8_t ENERGY_SENSOR_START = 9; -static const uint8_t CALIBRATE_REPORT_INTERVAL = 4; -static const int CALIBRATE_VERSION_MIN = 154; -static const std::string OP_NORMAL_MODE_STRING = "Normal"; -static const std::string OP_SIMPLE_MODE_STRING = "Simple"; - -enum OpModeStruct : uint8_t { OP_NORMAL_MODE = 1, OP_CALIBRATE_MODE = 2, OP_SIMPLE_MODE = 3 }; -static const std::map OP_MODE_TO_UINT{ - {"Normal", OP_NORMAL_MODE}, {"Calibrate", OP_CALIBRATE_MODE}, {"Simple", OP_SIMPLE_MODE}}; -static constexpr const char *ERR_MESSAGE[] = {"None", "Unknown", "Timeout"}; +enum OpMode : uint8_t { + OP_NORMAL_MODE = 1, + OP_CALIBRATE_MODE = 2, + OP_SIMPLE_MODE = 3, +}; class LD2420Listener { public: @@ -109,6 +39,23 @@ class LD2420Listener { class LD2420Component : public Component, public uart::UARTDevice { public: + struct CmdFrameT { + uint32_t header{0}; + uint32_t footer{0}; + uint16_t length{0}; + uint16_t command{0}; + uint16_t data_length{0}; + uint8_t data[18]; + }; + + struct RegConfigT { + uint32_t move_thresh[TOTAL_GATES]; + uint32_t still_thresh[TOTAL_GATES]; + uint16_t min_gate{0}; + uint16_t max_gate{0}; + uint16_t timeout{0}; + }; + void setup() override; void dump_config() override; void loop() override; @@ -150,23 +97,6 @@ class LD2420Component : public Component, public uart::UARTDevice { #endif void register_listener(LD2420Listener *listener) { this->listeners_.push_back(listener); } - struct CmdFrameT { - uint32_t header{0}; - uint16_t length{0}; - uint16_t command{0}; - uint8_t data[18]; - uint16_t data_length{0}; - uint32_t footer{0}; - }; - - struct RegConfigT { - uint16_t min_gate{0}; - uint16_t max_gate{0}; - uint16_t timeout{0}; - uint32_t move_thresh[LD2420_TOTAL_GATES]; - uint32_t still_thresh[LD2420_TOTAL_GATES]; - }; - void send_module_restart(); void restart_module_action(); void apply_config_action(); @@ -179,23 +109,28 @@ class LD2420Component : public Component, public uart::UARTDevice { void set_operating_mode(const std::string &state); void auto_calibrate_sensitivity(); void update_radar_data(uint16_t const *gate_energy, uint8_t sample_number); - static uint8_t calc_checksum(void *data, size_t size); + uint8_t set_config_mode(bool enable); + void set_min_max_distances_timeout(uint32_t max_gate_distance, uint32_t min_gate_distance, uint32_t timeout); + void set_gate_threshold(uint8_t gate); + void set_reg_value(uint16_t reg, uint16_t value); + void set_system_mode(uint16_t mode); + void ld2420_restart(); - RegConfigT current_config; - RegConfigT new_config; + float gate_move_sensitivity_factor{0.5}; + float gate_still_sensitivity_factor{0.5}; int32_t last_periodic_millis = millis(); int32_t report_periodic_millis = millis(); int32_t monitor_periodic_millis = millis(); int32_t last_normal_periodic_millis = millis(); - bool output_energy_state{false}; - uint8_t current_operating_mode{OP_NORMAL_MODE}; - uint16_t radar_data[LD2420_TOTAL_GATES][CALIBRATE_SAMPLES]; - uint16_t gate_avg[LD2420_TOTAL_GATES]; - uint16_t gate_peak[LD2420_TOTAL_GATES]; - uint8_t sample_number_counter{0}; + uint16_t radar_data[TOTAL_GATES][CALIBRATE_SAMPLES]; + uint16_t gate_avg[TOTAL_GATES]; + uint16_t gate_peak[TOTAL_GATES]; uint16_t total_sample_number_counter{0}; - float gate_move_sensitivity_factor{0.5}; - float gate_still_sensitivity_factor{0.5}; + uint8_t current_operating_mode{OP_NORMAL_MODE}; + uint8_t sample_number_counter{0}; + bool output_energy_state{false}; + RegConfigT current_config; + RegConfigT new_config; #ifdef USE_SELECT select::Select *operating_selector_{nullptr}; #endif @@ -205,24 +140,17 @@ class LD2420Component : public Component, public uart::UARTDevice { button::Button *restart_module_button_{nullptr}; button::Button *factory_reset_button_{nullptr}; #endif - void set_min_max_distances_timeout(uint32_t max_gate_distance, uint32_t min_gate_distance, uint32_t timeout); - void set_gate_threshold(uint8_t gate); - void set_reg_value(uint16_t reg, uint16_t value); - uint8_t set_config_mode(bool enable); - void set_system_mode(uint16_t mode); - void ld2420_restart(); protected: struct CmdReplyT { + uint32_t data[4]; + uint16_t error; uint8_t command; uint8_t status; - uint32_t data[4]; uint8_t length; - uint16_t error; volatile bool ack; }; - static int get_firmware_int(const char *version_string); void get_firmware_version_(); int get_gate_threshold_(uint8_t gate); void get_reg_value_(uint16_t reg); @@ -253,17 +181,17 @@ class LD2420Component : public Component, public uart::UARTDevice { std::vector gate_move_threshold_numbers_ = std::vector(16); #endif - uint16_t gate_energy_[LD2420_TOTAL_GATES]; - CmdReplyT cmd_reply_; uint32_t max_distance_gate_; uint32_t min_distance_gate_; - uint16_t system_mode_{CMD_SYSTEM_MODE_ENERGY}; - bool cmd_active_{false}; - char ld2420_firmware_ver_[8]{"v0.0.0"}; - bool presence_{false}; - bool calibration_{false}; + uint16_t system_mode_; + uint16_t gate_energy_[TOTAL_GATES]; uint16_t distance_{0}; uint8_t config_checksum_{0}; + char firmware_ver_[8]{"v0.0.0"}; + bool cmd_active_{false}; + bool presence_{false}; + bool calibration_{false}; + CmdReplyT cmd_reply_; std::vector listeners_{}; }; diff --git a/esphome/components/ld2420/sensor/ld2420_sensor.h b/esphome/components/ld2420/sensor/ld2420_sensor.h index 4eebefe0e3..82730d60e3 100644 --- a/esphome/components/ld2420/sensor/ld2420_sensor.h +++ b/esphome/components/ld2420/sensor/ld2420_sensor.h @@ -27,7 +27,7 @@ class LD2420Sensor : public LD2420Listener, public Component, sensor::Sensor { protected: sensor::Sensor *distance_sensor_{nullptr}; - std::vector energy_sensors_ = std::vector(LD2420_TOTAL_GATES); + std::vector energy_sensors_ = std::vector(TOTAL_GATES); }; } // namespace ld2420 From aaa7117ec9c0f8b20163d603edc78c3e7e9c6c96 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 Jun 2025 03:47:04 -0500 Subject: [PATCH 183/293] Update libsodium to 1.0.20 (#9240) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/api/__init__.py | 2 +- platformio.ini | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index ae83129c21..b02a875d72 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -182,7 +182,7 @@ async def to_code(config): # and plaintext disabled. Only a factory reset can remove it. cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_NOISE") - cg.add_library("esphome/noise-c", "0.1.6") + cg.add_library("esphome/noise-c", "0.1.10") else: cg.add_define("USE_API_PLAINTEXT") diff --git a/platformio.ini b/platformio.ini index 79e22f90b0..0d67e23222 100644 --- a/platformio.ini +++ b/platformio.ini @@ -33,7 +33,7 @@ build_flags = ; This are common settings for all environments. [common] lib_deps = - esphome/noise-c@0.1.4 ; api + esphome/noise-c@0.1.10 ; api improv/Improv@1.2.4 ; improv_serial / esp32_improv bblanchon/ArduinoJson@6.18.5 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code @@ -567,7 +567,7 @@ build_unflags = extends = common platform = platformio/native lib_deps = - esphome/noise-c@0.1.1 ; used by api + esphome/noise-c@0.1.10 ; used by api build_flags = ${common.build_flags} -DUSE_HOST From 8014cbc71e8f5c1f47a1d04d0c7b1aee9024e047 Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Mon, 30 Jun 2025 19:25:54 +0100 Subject: [PATCH 184/293] Fixes for async MQTT (#9273) --- esphome/components/mqtt/mqtt_backend_esp32.cpp | 8 ++++++++ esphome/components/mqtt/mqtt_backend_esp32.h | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index 62b153e676..4648e66e1d 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -163,12 +163,20 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { case MQTT_EVENT_CONNECTED: ESP_LOGV(TAG, "MQTT_EVENT_CONNECTED"); this->is_connected_ = true; +#if defined(USE_MQTT_IDF_ENQUEUE) + this->last_dropped_log_time_ = 0; + xTaskNotifyGive(this->task_handle_); +#endif this->on_connect_.call(event.session_present); break; case MQTT_EVENT_DISCONNECTED: ESP_LOGV(TAG, "MQTT_EVENT_DISCONNECTED"); // TODO is there a way to get the disconnect reason? this->is_connected_ = false; +#if defined(USE_MQTT_IDF_ENQUEUE) + this->last_dropped_log_time_ = 0; + xTaskNotifyGive(this->task_handle_); +#endif this->on_disconnect_.call(MQTTClientDisconnectReason::TCP_DISCONNECTED); break; diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index 57286a24b2..3611caf554 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -116,7 +116,7 @@ struct QueueElement { class MQTTBackendESP32 final : public MQTTBackend { public: static const size_t MQTT_BUFFER_SIZE = 4096; - static const size_t TASK_STACK_SIZE = 2048; + static const size_t TASK_STACK_SIZE = 3072; static const size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations static const ssize_t TASK_PRIORITY = 5; static const uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 From 0cbb5e6c1c67a436dcf9606417a18387c025e14d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 Jun 2025 15:02:43 -0500 Subject: [PATCH 185/293] Fix flaky test_api_conditional_memory by waiting for all required states (#9271) --- .../integration/test_api_conditional_memory.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/integration/test_api_conditional_memory.py b/tests/integration/test_api_conditional_memory.py index b85e8d91af..8048624f70 100644 --- a/tests/integration/test_api_conditional_memory.py +++ b/tests/integration/test_api_conditional_memory.py @@ -177,19 +177,22 @@ async def test_api_conditional_memory( async with api_client_connected() as client2: # Subscribe to states with new client states2: dict[int, EntityState] = {} - connected_future: asyncio.Future[None] = loop.create_future() + states_ready_future: asyncio.Future[None] = loop.create_future() def on_state2(state: EntityState) -> None: states2[state.key] = state - # Check for reconnection - if state.key == client_connected.key and state.state is True: - if not connected_future.done(): - connected_future.set_result(None) + # Check if we have received both required states + if ( + client_connected.key in states2 + and client_disconnected_event.key in states2 + and not states_ready_future.done() + ): + states_ready_future.set_result(None) client2.subscribe_states(on_state2) - # Wait for connected state - await asyncio.wait_for(connected_future, timeout=5.0) + # Wait for both connected and disconnected event states + await asyncio.wait_for(states_ready_future, timeout=5.0) # Verify client is connected again (on_client_connected fired) assert states2[client_connected.key].state is True, ( From 98e106e0ae9b30937ff87b5285d61c03fd2ce61a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 1 Jul 2025 08:09:11 +1200 Subject: [PATCH 186/293] [pins] Update ``internal_gpio_pin_number`` to work directly like ``internal_gpio_output_pin_number`` (#9270) --- esphome/components/i2c/__init__.py | 11 ++--------- esphome/pins.py | 4 +++- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index fae1fa1d22..6adb9b71aa 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -9,8 +9,6 @@ from esphome.const import ( CONF_FREQUENCY, CONF_I2C_ID, CONF_ID, - CONF_INPUT, - CONF_OUTPUT, CONF_SCAN, CONF_SCL, CONF_SDA, @@ -73,20 +71,15 @@ def validate_config(config): return config -pin_with_input_and_output_support = pins.internal_gpio_pin_number( - {CONF_OUTPUT: True, CONF_INPUT: True} -) - - CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): _bus_declare_type, - cv.Optional(CONF_SDA, default="SDA"): pin_with_input_and_output_support, + cv.Optional(CONF_SDA, default="SDA"): pins.internal_gpio_pin_number, cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32_idf=True): cv.All( cv.only_with_esp_idf, cv.boolean ), - cv.Optional(CONF_SCL, default="SCL"): pin_with_input_and_output_support, + cv.Optional(CONF_SCL, default="SCL"): pins.internal_gpio_pin_number, cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All( cv.only_with_esp_idf, cv.boolean ), diff --git a/esphome/pins.py b/esphome/pins.py index 0dfd5a245b..4f9b4859a1 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -220,7 +220,9 @@ def gpio_flags_expr(mode): gpio_pin_schema = _schema_creator -internal_gpio_pin_number = _internal_number_creator +internal_gpio_pin_number = _internal_number_creator( + {CONF_OUTPUT: True, CONF_INPUT: True} +) gpio_output_pin_schema = _schema_creator( { CONF_OUTPUT: True, From 78c8cd4c4e4f08c886a3faf44e1e5fa017e5755f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 1 Jul 2025 08:50:19 +1200 Subject: [PATCH 187/293] [http_request.update] Fix ``size_t`` printing (#9144) --- esphome/components/http_request/update/http_request_update.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 828fb5bd8b..6bc88ae49a 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -57,7 +57,7 @@ void HttpRequestUpdate::update_task(void *params) { RAMAllocator allocator; uint8_t *data = allocator.allocate(container->content_length); if (data == nullptr) { - std::string msg = str_sprintf("Failed to allocate %d bytes for manifest", container->content_length); + std::string msg = str_sprintf("Failed to allocate %zu bytes for manifest", container->content_length); this_update->status_set_error(msg.c_str()); container->end(); UPDATE_RETURN; From 08c88ba0f26d5b5558b4679ca9beba73acc20da3 Mon Sep 17 00:00:00 2001 From: piechade Date: Mon, 30 Jun 2025 22:54:23 +0200 Subject: [PATCH 188/293] [smt100] Rename ``dielectric_constant`` to ``permittivity`` (#9175) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/smt100/sensor.py | 12 ++++++++---- esphome/components/smt100/smt100.cpp | 10 +++++----- esphome/components/smt100/smt100.h | 6 +++--- esphome/const.py | 1 + tests/components/smt100/common.yaml | 4 ++-- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/esphome/components/smt100/sensor.py b/esphome/components/smt100/sensor.py index ea42499379..f877ce2af0 100644 --- a/esphome/components/smt100/sensor.py +++ b/esphome/components/smt100/sensor.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_DIELECTRIC_CONSTANT, CONF_ID, CONF_MOISTURE, + CONF_PERMITTIVITY, CONF_TEMPERATURE, CONF_VOLTAGE, DEVICE_CLASS_TEMPERATURE, @@ -33,7 +34,10 @@ CONFIG_SCHEMA = ( accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Optional(CONF_DIELECTRIC_CONSTANT): sensor.sensor_schema( + cv.Optional(CONF_DIELECTRIC_CONSTANT): cv.invalid( + "Use 'permittivity' instead" + ), + cv.Optional(CONF_PERMITTIVITY): sensor.sensor_schema( unit_of_measurement=UNIT_EMPTY, accuracy_decimals=2, state_class=STATE_CLASS_MEASUREMENT, @@ -76,9 +80,9 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_COUNTS]) cg.add(var.set_counts_sensor(sens)) - if CONF_DIELECTRIC_CONSTANT in config: - sens = await sensor.new_sensor(config[CONF_DIELECTRIC_CONSTANT]) - cg.add(var.set_dielectric_constant_sensor(sens)) + if CONF_PERMITTIVITY in config: + sens = await sensor.new_sensor(config[CONF_PERMITTIVITY]) + cg.add(var.set_permittivity_sensor(sens)) if CONF_TEMPERATURE in config: sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) diff --git a/esphome/components/smt100/smt100.cpp b/esphome/components/smt100/smt100.cpp index 24ba05b894..c8dfb4c7bd 100644 --- a/esphome/components/smt100/smt100.cpp +++ b/esphome/components/smt100/smt100.cpp @@ -16,7 +16,7 @@ void SMT100Component::loop() { while (this->available() != 0) { if (readline_(read(), buffer, MAX_LINE_LENGTH) > 0) { int counts = (int) strtol((strtok(buffer, ",")), nullptr, 10); - float dielectric_constant = (float) strtod((strtok(nullptr, ",")), nullptr); + float permittivity = (float) strtod((strtok(nullptr, ",")), nullptr); float moisture = (float) strtod((strtok(nullptr, ",")), nullptr); float temperature = (float) strtod((strtok(nullptr, ",")), nullptr); float voltage = (float) strtod((strtok(nullptr, ",")), nullptr); @@ -25,8 +25,8 @@ void SMT100Component::loop() { counts_sensor_->publish_state(counts); } - if (this->dielectric_constant_sensor_ != nullptr) { - dielectric_constant_sensor_->publish_state(dielectric_constant); + if (this->permittivity_sensor_ != nullptr) { + permittivity_sensor_->publish_state(permittivity); } if (this->moisture_sensor_ != nullptr) { @@ -49,8 +49,8 @@ float SMT100Component::get_setup_priority() const { return setup_priority::DATA; void SMT100Component::dump_config() { ESP_LOGCONFIG(TAG, "SMT100:"); - LOG_SENSOR(TAG, "Counts", this->temperature_sensor_); - LOG_SENSOR(TAG, "Dielectric Constant", this->temperature_sensor_); + LOG_SENSOR(TAG, "Counts", this->counts_sensor_); + LOG_SENSOR(TAG, "Permittivity", this->permittivity_sensor_); LOG_SENSOR(TAG, "Temperature", this->temperature_sensor_); LOG_SENSOR(TAG, "Moisture", this->moisture_sensor_); LOG_UPDATE_INTERVAL(this); diff --git a/esphome/components/smt100/smt100.h b/esphome/components/smt100/smt100.h index 017818bdcf..86827607dc 100644 --- a/esphome/components/smt100/smt100.h +++ b/esphome/components/smt100/smt100.h @@ -20,8 +20,8 @@ class SMT100Component : public PollingComponent, public uart::UARTDevice { float get_setup_priority() const override; void set_counts_sensor(sensor::Sensor *counts_sensor) { this->counts_sensor_ = counts_sensor; } - void set_dielectric_constant_sensor(sensor::Sensor *dielectric_constant_sensor) { - this->dielectric_constant_sensor_ = dielectric_constant_sensor; + void set_permittivity_sensor(sensor::Sensor *permittivity_sensor) { + this->permittivity_sensor_ = permittivity_sensor; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } void set_moisture_sensor(sensor::Sensor *moisture_sensor) { this->moisture_sensor_ = moisture_sensor; } @@ -31,7 +31,7 @@ class SMT100Component : public PollingComponent, public uart::UARTDevice { int readline_(int readch, char *buffer, int len); sensor::Sensor *counts_sensor_{nullptr}; - sensor::Sensor *dielectric_constant_sensor_{nullptr}; + sensor::Sensor *permittivity_sensor_{nullptr}; sensor::Sensor *moisture_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *voltage_sensor_{nullptr}; diff --git a/esphome/const.py b/esphome/const.py index b167935d12..4aeb5179e6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -654,6 +654,7 @@ CONF_PAYLOAD = "payload" CONF_PAYLOAD_AVAILABLE = "payload_available" CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available" CONF_PERIOD = "period" +CONF_PERMITTIVITY = "permittivity" CONF_PH = "ph" CONF_PHASE_A = "phase_a" CONF_PHASE_ANGLE = "phase_angle" diff --git a/tests/components/smt100/common.yaml b/tests/components/smt100/common.yaml index f86bd762e7..b12d7198fd 100644 --- a/tests/components/smt100/common.yaml +++ b/tests/components/smt100/common.yaml @@ -8,8 +8,8 @@ sensor: - platform: smt100 counts: name: Counts - dielectric_constant: - name: Dielectric Constant + permittivity: + name: Permittivity temperature: name: Temperature moisture: From e58baab563ed9a93dd3f55edfaac2b6b7be4af03 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 30 Jun 2025 18:06:59 -0400 Subject: [PATCH 189/293] [ethernet] P4 changes and 5.3.0 deprecated warnings (#8457) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/ethernet/__init__.py | 55 ++++++++++++------- .../ethernet/ethernet_component.cpp | 41 +++++++++----- .../components/ethernet/ethernet_component.h | 5 +- tests/components/ethernet/common-dp83848.yaml | 4 +- tests/components/ethernet/common-ip101.yaml | 4 +- tests/components/ethernet/common-jl1101.yaml | 4 +- tests/components/ethernet/common-ksz8081.yaml | 4 +- .../ethernet/common-ksz8081rna.yaml | 4 +- tests/components/ethernet/common-lan8720.yaml | 4 +- tests/components/ethernet/common-rtl8201.yaml | 4 +- tests/components/ethernet_info/common.yaml | 4 +- 11 files changed, 88 insertions(+), 45 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 8eec9510cc..ac07d02e37 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -23,8 +23,10 @@ from esphome.const import ( CONF_INTERRUPT_PIN, CONF_MANUAL_IP, CONF_MISO_PIN, + CONF_MODE, CONF_MOSI_PIN, CONF_PAGE_ID, + CONF_PIN, CONF_POLLING_INTERVAL, CONF_RESET_PIN, CONF_SPI, @@ -49,6 +51,7 @@ PHYRegister = ethernet_ns.struct("PHYRegister") CONF_PHY_ADDR = "phy_addr" CONF_MDC_PIN = "mdc_pin" CONF_MDIO_PIN = "mdio_pin" +CONF_CLK = "clk" CONF_CLK_MODE = "clk_mode" CONF_POWER_PIN = "power_pin" CONF_PHY_REGISTERS = "phy_registers" @@ -73,26 +76,18 @@ SPI_ETHERNET_TYPES = ["W5500", "DM9051"] SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10) emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") -emac_rmii_clock_gpio_t = cg.global_ns.enum("emac_rmii_clock_gpio_t") + CLK_MODES = { - "GPIO0_IN": ( - emac_rmii_clock_mode_t.EMAC_CLK_EXT_IN, - emac_rmii_clock_gpio_t.EMAC_CLK_IN_GPIO, - ), - "GPIO0_OUT": ( - emac_rmii_clock_mode_t.EMAC_CLK_OUT, - emac_rmii_clock_gpio_t.EMAC_APPL_CLK_OUT_GPIO, - ), - "GPIO16_OUT": ( - emac_rmii_clock_mode_t.EMAC_CLK_OUT, - emac_rmii_clock_gpio_t.EMAC_CLK_OUT_GPIO, - ), - "GPIO17_OUT": ( - emac_rmii_clock_mode_t.EMAC_CLK_OUT, - emac_rmii_clock_gpio_t.EMAC_CLK_OUT_180_GPIO, - ), + "CLK_EXT_IN": emac_rmii_clock_mode_t.EMAC_CLK_EXT_IN, + "CLK_OUT": emac_rmii_clock_mode_t.EMAC_CLK_OUT, } +CLK_MODES_DEPRECATED = { + "GPIO0_IN": ("CLK_EXT_IN", 0), + "GPIO0_OUT": ("CLK_OUT", 0), + "GPIO16_OUT": ("CLK_OUT", 16), + "GPIO17_OUT": ("CLK_OUT", 17), +} MANUAL_IP_SCHEMA = cv.Schema( { @@ -154,6 +149,18 @@ def _validate(config): f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), " f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]." ) + elif config[CONF_TYPE] != "OPENETH": + if CONF_CLK_MODE in config: + LOGGER.warning( + "[ethernet] The 'clk_mode' option is deprecated and will be removed in ESPHome 2026.1. " + "Please update your configuration to use 'clk' instead." + ) + mode = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] + config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode[0], CONF_PIN: mode[1]}) + del config[CONF_CLK_MODE] + elif CONF_CLK not in config: + raise cv.Invalid("'clk' is a required option for [ethernet].") + return config @@ -177,14 +184,21 @@ PHY_REGISTER_SCHEMA = cv.Schema( cv.Optional(CONF_PAGE_ID): cv.hex_int, } ) +CLK_SCHEMA = cv.Schema( + { + cv.Required(CONF_MODE): cv.enum(CLK_MODES, upper=True, space="_"), + cv.Required(CONF_PIN): pins.internal_gpio_pin_number, + } +) RMII_SCHEMA = BASE_SCHEMA.extend( cv.Schema( { cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number, cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_CLK_MODE, default="GPIO0_IN"): cv.enum( - CLK_MODES, upper=True, space="_" + cv.Optional(CONF_CLK_MODE): cv.enum( + CLK_MODES_DEPRECATED, upper=True, space="_" ), + cv.Optional(CONF_CLK): CLK_SCHEMA, cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31), cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number, cv.Optional(CONF_PHY_REGISTERS): cv.ensure_list(PHY_REGISTER_SCHEMA), @@ -308,7 +322,8 @@ async def to_code(config): cg.add(var.set_phy_addr(config[CONF_PHY_ADDR])) cg.add(var.set_mdc_pin(config[CONF_MDC_PIN])) cg.add(var.set_mdio_pin(config[CONF_MDIO_PIN])) - cg.add(var.set_clk_mode(*CLK_MODES[config[CONF_CLK_MODE]])) + cg.add(var.set_clk_mode(config[CONF_CLK][CONF_MODE])) + cg.add(var.set_clk_pin(config[CONF_CLK][CONF_PIN])) if CONF_POWER_PIN in config: cg.add(var.set_power_pin(config[CONF_POWER_PIN])) for register_value in config.get(CONF_PHY_REGISTERS, []): diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 8739269f4a..19a11c6945 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -17,6 +17,22 @@ namespace esphome { namespace ethernet { +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) +// work around IDF compile issue on P4 https://github.com/espressif/esp-idf/pull/15637 +#ifdef USE_ESP32_VARIANT_ESP32P4 +#undef ETH_ESP32_EMAC_DEFAULT_CONFIG +#define ETH_ESP32_EMAC_DEFAULT_CONFIG() \ + { \ + .smi_gpio = {.mdc_num = 31, .mdio_num = 52}, .interface = EMAC_DATA_INTERFACE_RMII, \ + .clock_config = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = (emac_rmii_clock_gpio_t) 50}}, \ + .dma_burst_len = ETH_DMA_BURST_LEN_32, .intr_priority = 0, \ + .emac_dataif_gpio = \ + {.rmii = {.tx_en_num = 49, .txd0_num = 34, .txd1_num = 35, .crs_dv_num = 28, .rxd0_num = 29, .rxd1_num = 30}}, \ + .clock_config_out_in = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = (emac_rmii_clock_gpio_t) -1}}, \ + } +#endif +#endif + static const char *const TAG = "ethernet"; EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -150,22 +166,18 @@ void EthernetComponent::setup() { phy_config.phy_addr = this->phy_addr_; phy_config.reset_gpio_num = this->power_pin_; -#if ESP_IDF_VERSION_MAJOR >= 5 eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG(); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + esp32_emac_config.smi_gpio.mdc_num = this->mdc_pin_; + esp32_emac_config.smi_gpio.mdio_num = this->mdio_pin_; +#else esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_; esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_; +#endif esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_; - esp32_emac_config.clock_config.rmii.clock_gpio = this->clk_gpio_; + esp32_emac_config.clock_config.rmii.clock_gpio = (emac_rmii_clock_gpio_t) this->clk_pin_; esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config); -#else - mac_config.smi_mdc_gpio_num = this->mdc_pin_; - mac_config.smi_mdio_gpio_num = this->mdio_pin_; - mac_config.clock_config.rmii.clock_mode = this->clk_mode_; - mac_config.clock_config.rmii.clock_gpio = this->clk_gpio_; - - esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&mac_config); -#endif #endif switch (this->type_) { @@ -387,10 +399,11 @@ void EthernetComponent::dump_config() { ESP_LOGCONFIG(TAG, " Power Pin: %u", this->power_pin_); } ESP_LOGCONFIG(TAG, + " CLK Pin: %u\n" " MDC Pin: %u\n" " MDIO Pin: %u\n" " PHY addr: %u", - this->mdc_pin_, this->mdio_pin_, this->phy_addr_); + this->clk_pin_, this->mdc_pin_, this->mdio_pin_, this->phy_addr_); #endif ESP_LOGCONFIG(TAG, " Type: %s", eth_type); } @@ -611,10 +624,8 @@ void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_a void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; } void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; } void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; } -void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_clock_gpio_t clk_gpio) { - this->clk_mode_ = clk_mode; - this->clk_gpio_ = clk_gpio; -} +void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } +void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; } void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); } #endif void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 6cdc113aa8..1b347946f5 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -76,7 +76,8 @@ class EthernetComponent : public Component { void set_power_pin(int power_pin); void set_mdc_pin(uint8_t mdc_pin); void set_mdio_pin(uint8_t mdio_pin); - void set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_clock_gpio_t clk_gpio); + void set_clk_pin(uint8_t clk_pin); + void set_clk_mode(emac_rmii_clock_mode_t clk_mode); void add_phy_register(PHYRegister register_value); #endif void set_type(EthernetType type); @@ -123,10 +124,10 @@ class EthernetComponent : public Component { // Group all 32-bit members first int power_pin_{-1}; emac_rmii_clock_mode_t clk_mode_{EMAC_CLK_EXT_IN}; - emac_rmii_clock_gpio_t clk_gpio_{EMAC_CLK_IN_GPIO}; std::vector phy_registers_{}; // Group all 8-bit members together + uint8_t clk_pin_{0}; uint8_t phy_addr_{0}; uint8_t mdc_pin_{23}; uint8_t mdio_pin_{18}; diff --git a/tests/components/ethernet/common-dp83848.yaml b/tests/components/ethernet/common-dp83848.yaml index 5b6ed3e8d0..140c7d0d1b 100644 --- a/tests/components/ethernet/common-dp83848.yaml +++ b/tests/components/ethernet/common-dp83848.yaml @@ -2,7 +2,9 @@ ethernet: type: DP83848 mdc_pin: 23 mdio_pin: 25 - clk_mode: GPIO0_IN + clk: + pin: 0 + mode: CLK_EXT_IN phy_addr: 0 power_pin: 26 manual_ip: diff --git a/tests/components/ethernet/common-ip101.yaml b/tests/components/ethernet/common-ip101.yaml index 5ca369cce1..b5589220de 100644 --- a/tests/components/ethernet/common-ip101.yaml +++ b/tests/components/ethernet/common-ip101.yaml @@ -2,7 +2,9 @@ ethernet: type: IP101 mdc_pin: 23 mdio_pin: 25 - clk_mode: GPIO0_IN + clk: + pin: 0 + mode: CLK_EXT_IN phy_addr: 0 power_pin: 26 manual_ip: diff --git a/tests/components/ethernet/common-jl1101.yaml b/tests/components/ethernet/common-jl1101.yaml index 639542d807..2ada9495a0 100644 --- a/tests/components/ethernet/common-jl1101.yaml +++ b/tests/components/ethernet/common-jl1101.yaml @@ -2,7 +2,9 @@ ethernet: type: JL1101 mdc_pin: 23 mdio_pin: 25 - clk_mode: GPIO0_IN + clk: + pin: 0 + mode: CLK_EXT_IN phy_addr: 0 power_pin: 26 manual_ip: diff --git a/tests/components/ethernet/common-ksz8081.yaml b/tests/components/ethernet/common-ksz8081.yaml index 167606a1eb..7da8adb09a 100644 --- a/tests/components/ethernet/common-ksz8081.yaml +++ b/tests/components/ethernet/common-ksz8081.yaml @@ -2,7 +2,9 @@ ethernet: type: KSZ8081 mdc_pin: 23 mdio_pin: 25 - clk_mode: GPIO0_IN + clk: + pin: 0 + mode: CLK_EXT_IN phy_addr: 0 power_pin: 26 manual_ip: diff --git a/tests/components/ethernet/common-ksz8081rna.yaml b/tests/components/ethernet/common-ksz8081rna.yaml index f506906b1b..df04f06132 100644 --- a/tests/components/ethernet/common-ksz8081rna.yaml +++ b/tests/components/ethernet/common-ksz8081rna.yaml @@ -2,7 +2,9 @@ ethernet: type: KSZ8081RNA mdc_pin: 23 mdio_pin: 25 - clk_mode: GPIO0_IN + clk: + pin: 0 + mode: CLK_EXT_IN phy_addr: 0 power_pin: 26 manual_ip: diff --git a/tests/components/ethernet/common-lan8720.yaml b/tests/components/ethernet/common-lan8720.yaml index b9ed9cb036..f227752f42 100644 --- a/tests/components/ethernet/common-lan8720.yaml +++ b/tests/components/ethernet/common-lan8720.yaml @@ -2,7 +2,9 @@ ethernet: type: LAN8720 mdc_pin: 23 mdio_pin: 25 - clk_mode: GPIO0_IN + clk: + pin: 0 + mode: CLK_EXT_IN phy_addr: 0 power_pin: 26 manual_ip: diff --git a/tests/components/ethernet/common-rtl8201.yaml b/tests/components/ethernet/common-rtl8201.yaml index 43842e7c9f..7c9c9d913c 100644 --- a/tests/components/ethernet/common-rtl8201.yaml +++ b/tests/components/ethernet/common-rtl8201.yaml @@ -2,7 +2,9 @@ ethernet: type: RTL8201 mdc_pin: 23 mdio_pin: 25 - clk_mode: GPIO0_IN + clk: + pin: 0 + mode: CLK_EXT_IN phy_addr: 0 power_pin: 26 manual_ip: diff --git a/tests/components/ethernet_info/common.yaml b/tests/components/ethernet_info/common.yaml index d9a6f515b1..f45f345316 100644 --- a/tests/components/ethernet_info/common.yaml +++ b/tests/components/ethernet_info/common.yaml @@ -2,7 +2,9 @@ ethernet: type: LAN8720 mdc_pin: 23 mdio_pin: 25 - clk_mode: GPIO0_IN + clk: + pin: 0 + mode: CLK_EXT_IN phy_addr: 0 power_pin: 26 manual_ip: From db7a420e54059217458be57ce3be40a220fd204b Mon Sep 17 00:00:00 2001 From: Mathieu Rene Date: Mon, 30 Jun 2025 18:07:30 -0400 Subject: [PATCH 190/293] Fix - Pass thread TLVs down to openthread if they are defined (#9182) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/openthread/__init__.py | 72 +++++++++---------- .../components/openthread/openthread_esp.cpp | 32 +++++++-- esphome/components/openthread/tlv.py | 65 ----------------- 3 files changed, 62 insertions(+), 107 deletions(-) delete mode 100644 esphome/components/openthread/tlv.py diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 393c47e720..65138e28c7 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -22,7 +22,6 @@ from .const import ( CONF_SRP_ID, CONF_TLV, ) -from .tlv import parse_tlv CODEOWNERS = ["@mrene"] @@ -43,29 +42,40 @@ def set_sdkconfig_options(config): add_idf_sdkconfig_option("CONFIG_OPENTHREAD_CLI", False) add_idf_sdkconfig_option("CONFIG_OPENTHREAD_ENABLED", True) - add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", config[CONF_PAN_ID]) - add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_CHANNEL", config[CONF_CHANNEL]) - add_idf_sdkconfig_option( - "CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{config[CONF_NETWORK_KEY]:X}".lower() - ) - if network_name := config.get(CONF_NETWORK_NAME): - add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_NAME", network_name) + if tlv := config.get(CONF_TLV): + cg.add_define("USE_OPENTHREAD_TLVS", tlv) + else: + if pan_id := config.get(CONF_PAN_ID): + add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", pan_id) - if (ext_pan_id := config.get(CONF_EXT_PAN_ID)) is not None: - add_idf_sdkconfig_option( - "CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}".lower() - ) - if (mesh_local_prefix := config.get(CONF_MESH_LOCAL_PREFIX)) is not None: - add_idf_sdkconfig_option( - "CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix}".lower() - ) - if (pskc := config.get(CONF_PSKC)) is not None: - add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}".lower()) + if channel := config.get(CONF_CHANNEL): + add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_CHANNEL", channel) - if CONF_FORCE_DATASET in config: - if config[CONF_FORCE_DATASET]: - cg.add_define("CONFIG_OPENTHREAD_FORCE_DATASET") + if network_key := config.get(CONF_NETWORK_KEY): + add_idf_sdkconfig_option( + "CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{network_key:X}".lower() + ) + + if network_name := config.get(CONF_NETWORK_NAME): + add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_NAME", network_name) + + if (ext_pan_id := config.get(CONF_EXT_PAN_ID)) is not None: + add_idf_sdkconfig_option( + "CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}".lower() + ) + if (mesh_local_prefix := config.get(CONF_MESH_LOCAL_PREFIX)) is not None: + add_idf_sdkconfig_option( + "CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix}".lower() + ) + if (pskc := config.get(CONF_PSKC)) is not None: + add_idf_sdkconfig_option( + "CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}".lower() + ) + + if force_dataset := config.get(CONF_FORCE_DATASET): + if force_dataset: + cg.add_define("USE_OPENTHREAD_FORCE_DATASET") add_idf_sdkconfig_option("CONFIG_OPENTHREAD_DNS64_CLIENT", True) add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT", True) @@ -79,22 +89,11 @@ openthread_ns = cg.esphome_ns.namespace("openthread") OpenThreadComponent = openthread_ns.class_("OpenThreadComponent", cg.Component) OpenThreadSrpComponent = openthread_ns.class_("OpenThreadSrpComponent", cg.Component) - -def _convert_tlv(config): - if tlv := config.get(CONF_TLV): - config = config.copy() - parsed_tlv = parse_tlv(tlv) - validated = _CONNECTION_SCHEMA(parsed_tlv) - config.update(validated) - del config[CONF_TLV] - return config - - _CONNECTION_SCHEMA = cv.Schema( { - cv.Inclusive(CONF_PAN_ID, "manual"): cv.hex_int, - cv.Inclusive(CONF_CHANNEL, "manual"): cv.int_, - cv.Inclusive(CONF_NETWORK_KEY, "manual"): cv.hex_int, + cv.Optional(CONF_PAN_ID): cv.hex_int, + cv.Optional(CONF_CHANNEL): cv.int_, + cv.Optional(CONF_NETWORK_KEY): cv.hex_int, cv.Optional(CONF_EXT_PAN_ID): cv.hex_int, cv.Optional(CONF_NETWORK_NAME): cv.string_strict, cv.Optional(CONF_PSKC): cv.hex_int, @@ -112,8 +111,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_TLV): cv.string_strict, } ).extend(_CONNECTION_SCHEMA), - cv.has_exactly_one_key(CONF_PAN_ID, CONF_TLV), - _convert_tlv, + cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), cv.only_with_esp_idf, only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]), ) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index c5c817382f..dc303cef17 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -111,14 +111,36 @@ void OpenThreadComponent::ot_main() { esp_openthread_cli_create_task(); #endif ESP_LOGI(TAG, "Activating dataset..."); - otOperationalDatasetTlvs dataset; + otOperationalDatasetTlvs dataset = {}; -#ifdef CONFIG_OPENTHREAD_FORCE_DATASET - ESP_ERROR_CHECK(esp_openthread_auto_start(NULL)); -#else +#ifndef USE_OPENTHREAD_FORCE_DATASET + // Check if openthread has a valid dataset from a previous execution otError error = otDatasetGetActiveTlvs(esp_openthread_get_instance(), &dataset); - ESP_ERROR_CHECK(esp_openthread_auto_start((error == OT_ERROR_NONE) ? &dataset : NULL)); + if (error != OT_ERROR_NONE) { + // Make sure the length is 0 so we fallback to the configuration + dataset.mLength = 0; + } else { + ESP_LOGI(TAG, "Found OpenThread-managed dataset, ignoring esphome configuration"); + ESP_LOGI(TAG, "(set force_dataset: true to override)"); + } #endif + +#ifdef USE_OPENTHREAD_TLVS + if (dataset.mLength == 0) { + // If we didn't have an active dataset, and we have tlvs, parse it and pass it to esp_openthread_auto_start + size_t len = (sizeof(USE_OPENTHREAD_TLVS) - 1) / 2; + if (len > sizeof(dataset.mTlvs)) { + ESP_LOGW(TAG, "TLV buffer too small, truncating"); + len = sizeof(dataset.mTlvs); + } + parse_hex(USE_OPENTHREAD_TLVS, sizeof(USE_OPENTHREAD_TLVS) - 1, dataset.mTlvs, len); + dataset.mLength = len; + } +#endif + + // Pass the existing dataset, or NULL which will use the preprocessor definitions + ESP_ERROR_CHECK(esp_openthread_auto_start(dataset.mLength > 0 ? &dataset : nullptr)); + esp_openthread_launch_mainloop(); // Clean up diff --git a/esphome/components/openthread/tlv.py b/esphome/components/openthread/tlv.py deleted file mode 100644 index 4a7d21c47d..0000000000 --- a/esphome/components/openthread/tlv.py +++ /dev/null @@ -1,65 +0,0 @@ -# Sourced from https://gist.github.com/agners/0338576e0003318b63ec1ea75adc90f9 -import binascii -import ipaddress - -from esphome.const import CONF_CHANNEL - -from . import ( - CONF_EXT_PAN_ID, - CONF_MESH_LOCAL_PREFIX, - CONF_NETWORK_KEY, - CONF_NETWORK_NAME, - CONF_PAN_ID, - CONF_PSKC, -) - -TLV_TYPES = { - 0: CONF_CHANNEL, - 1: CONF_PAN_ID, - 2: CONF_EXT_PAN_ID, - 3: CONF_NETWORK_NAME, - 4: CONF_PSKC, - 5: CONF_NETWORK_KEY, - 7: CONF_MESH_LOCAL_PREFIX, -} - - -def parse_tlv(tlv) -> dict: - data = binascii.a2b_hex(tlv) - output = {} - pos = 0 - while pos < len(data): - tag = data[pos] - pos += 1 - _len = data[pos] - pos += 1 - val = data[pos : pos + _len] - pos += _len - if tag in TLV_TYPES: - if tag == 3: - output[TLV_TYPES[tag]] = val.decode("utf-8") - elif tag == 7: - mesh_local_prefix = binascii.hexlify(val).decode("utf-8") - mesh_local_prefix_str = f"{mesh_local_prefix}0000000000000000" - ipv6_bytes = bytes.fromhex(mesh_local_prefix_str) - ipv6_address = ipaddress.IPv6Address(ipv6_bytes) - output[TLV_TYPES[tag]] = f"{ipv6_address}/64" - else: - output[TLV_TYPES[tag]] = int.from_bytes(val) - return output - - -def main(): - import sys - - args = sys.argv[1:] - parsed = parse_tlv(args[0]) - # print the parsed TLV data - for key, value in parsed.items(): - if isinstance(value, bytes): - value = value.hex() - print(f"{key}: {value}") - - -if __name__ == "__main__": - main() From 9a0ba1657e0140b90a03aa9b1dcc1af4a4c61ff2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 Jun 2025 21:38:19 -0500 Subject: [PATCH 191/293] Fix entity hash collisions by enforcing unique names across devices per platform (#9276) --- esphome/core/entity_helpers.py | 22 +-- ...ies_not_allowed_on_different_devices.yaml} | 80 ++++++---- tests/integration/test_duplicate_entities.py | 137 +++++++++++------- tests/unit_tests/core/test_entity_helpers.py | 40 +++-- 4 files changed, 175 insertions(+), 104 deletions(-) rename tests/integration/fixtures/{duplicate_entities_on_different_devices.yaml => duplicate_entities_not_allowed_on_different_devices.yaml} (61%) diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index c95acebbf9..2442fbca4b 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -184,25 +184,27 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy # No name to validate return config - # Get the entity name and device info + # Get the entity name entity_name = config[CONF_NAME] - device_id = "" # Empty string for main device + # Get device name if entity is on a sub-device + device_name = None if CONF_DEVICE_ID in config: device_id_obj = config[CONF_DEVICE_ID] - # Use the device ID string directly for uniqueness - device_id = device_id_obj.id + device_name = device_id_obj.id - # For duplicate detection, just use the sanitized name - name_key = sanitize(snake_case(entity_name)) + # Calculate what object_id will actually be used + # This handles empty names correctly by using device/friendly names + name_key = get_base_entity_object_id( + entity_name, CORE.friendly_name, device_name + ) # Check for duplicates - unique_key = (device_id, platform, name_key) + unique_key = (platform, name_key) if unique_key in CORE.unique_ids: - device_prefix = f" on device '{device_id}'" if device_id else "" raise cv.Invalid( - f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. " - f"Each entity on a device must have a unique name within its platform." + f"Duplicate {platform} entity with name '{entity_name}' found. " + f"Each entity must have a unique name within its platform across all devices." ) # Add to tracking set diff --git a/tests/integration/fixtures/duplicate_entities_on_different_devices.yaml b/tests/integration/fixtures/duplicate_entities_not_allowed_on_different_devices.yaml similarity index 61% rename from tests/integration/fixtures/duplicate_entities_on_different_devices.yaml rename to tests/integration/fixtures/duplicate_entities_not_allowed_on_different_devices.yaml index ecc502ad28..f7d017a0ae 100644 --- a/tests/integration/fixtures/duplicate_entities_on_different_devices.yaml +++ b/tests/integration/fixtures/duplicate_entities_not_allowed_on_different_devices.yaml @@ -1,6 +1,6 @@ esphome: name: duplicate-entities-test - # Define devices to test multi-device duplicate handling + # Define devices to test multi-device unique name validation devices: - id: controller_1 name: Controller 1 @@ -13,31 +13,31 @@ host: api: # Port will be automatically injected logger: -# Test that duplicate entity names are allowed on different devices +# Test that duplicate entity names are NOT allowed on different devices -# Scenario 1: Same sensor name on different devices (allowed) +# Scenario 1: Different sensor names on different devices (allowed) sensor: - platform: template - name: Temperature + name: Temperature Controller 1 device_id: controller_1 lambda: return 21.0; update_interval: 0.1s - platform: template - name: Temperature + name: Temperature Controller 2 device_id: controller_2 lambda: return 22.0; update_interval: 0.1s - platform: template - name: Temperature + name: Temperature Controller 3 device_id: controller_3 lambda: return 23.0; update_interval: 0.1s # Main device sensor (no device_id) - platform: template - name: Temperature + name: Temperature Main lambda: return 20.0; update_interval: 0.1s @@ -47,20 +47,20 @@ sensor: lambda: return 60.0; update_interval: 0.1s -# Scenario 2: Same binary sensor name on different devices (allowed) +# Scenario 2: Different binary sensor names on different devices binary_sensor: - platform: template - name: Status + name: Status Controller 1 device_id: controller_1 lambda: return true; - platform: template - name: Status + name: Status Controller 2 device_id: controller_2 lambda: return false; - platform: template - name: Status + name: Status Main lambda: return true; # Main device # Different platform can have same name as sensor @@ -68,43 +68,43 @@ binary_sensor: name: Temperature lambda: return true; -# Scenario 3: Same text sensor name on different devices +# Scenario 3: Different text sensor names on different devices text_sensor: - platform: template - name: Device Info + name: Device Info Controller 1 device_id: controller_1 lambda: return {"Controller 1 Active"}; update_interval: 0.1s - platform: template - name: Device Info + name: Device Info Controller 2 device_id: controller_2 lambda: return {"Controller 2 Active"}; update_interval: 0.1s - platform: template - name: Device Info + name: Device Info Main lambda: return {"Main Device Active"}; update_interval: 0.1s -# Scenario 4: Same switch name on different devices +# Scenario 4: Different switch names on different devices switch: - platform: template - name: Power + name: Power Controller 1 device_id: controller_1 lambda: return false; turn_on_action: [] turn_off_action: [] - platform: template - name: Power + name: Power Controller 2 device_id: controller_2 lambda: return true; turn_on_action: [] turn_off_action: [] - platform: template - name: Power + name: Power Controller 3 device_id: controller_3 lambda: return false; turn_on_action: [] @@ -117,26 +117,54 @@ switch: turn_on_action: [] turn_off_action: [] -# Scenario 5: Empty names on different devices (should use device name) +# Scenario 5: Buttons with unique names button: - platform: template - name: "" + name: "Reset Controller 1" device_id: controller_1 on_press: [] - platform: template - name: "" + name: "Reset Controller 2" device_id: controller_2 on_press: [] - platform: template - name: "" + name: "Reset Main" on_press: [] # Main device -# Scenario 6: Special characters in names +# Scenario 6: Empty names (should use device names) +select: + - platform: template + name: "" + device_id: controller_1 + options: + - "Option 1" + - "Option 2" + lambda: return {"Option 1"}; + set_action: [] + + - platform: template + name: "" + device_id: controller_2 + options: + - "Option 1" + - "Option 2" + lambda: return {"Option 1"}; + set_action: [] + + - platform: template + name: "" # Main device + options: + - "Option 1" + - "Option 2" + lambda: return {"Option 1"}; + set_action: [] + +# Scenario 7: Special characters in names - now with unique names number: - platform: template - name: "Temperature Setpoint!" + name: "Temperature Setpoint! Controller 1" device_id: controller_1 min_value: 10.0 max_value: 30.0 @@ -145,7 +173,7 @@ number: set_action: [] - platform: template - name: "Temperature Setpoint!" + name: "Temperature Setpoint! Controller 2" device_id: controller_2 min_value: 10.0 max_value: 30.0 diff --git a/tests/integration/test_duplicate_entities.py b/tests/integration/test_duplicate_entities.py index 99968204d4..b7ee8dd478 100644 --- a/tests/integration/test_duplicate_entities.py +++ b/tests/integration/test_duplicate_entities.py @@ -11,12 +11,12 @@ from .types import APIClientConnectedFactory, RunCompiledFunction @pytest.mark.asyncio -async def test_duplicate_entities_on_different_devices( +async def test_duplicate_entities_not_allowed_on_different_devices( yaml_config: str, run_compiled: RunCompiledFunction, api_client_connected: APIClientConnectedFactory, ) -> None: - """Test that duplicate entity names are allowed on different devices.""" + """Test that duplicate entity names are NOT allowed on different devices.""" async with run_compiled(yaml_config), api_client_connected() as client: # Get device info device_info = await client.device_info() @@ -52,42 +52,46 @@ async def test_duplicate_entities_on_different_devices( switches = [e for e in all_entities if e.__class__.__name__ == "SwitchInfo"] buttons = [e for e in all_entities if e.__class__.__name__ == "ButtonInfo"] numbers = [e for e in all_entities if e.__class__.__name__ == "NumberInfo"] + selects = [e for e in all_entities if e.__class__.__name__ == "SelectInfo"] - # Scenario 1: Check sensors with same "Temperature" name on different devices - temp_sensors = [s for s in sensors if s.name == "Temperature"] + # Scenario 1: Check that temperature sensors have unique names per device + temp_sensors = [s for s in sensors if "Temperature" in s.name] assert len(temp_sensors) == 4, ( f"Expected exactly 4 temperature sensors, got {len(temp_sensors)}" ) - # Verify each sensor is on a different device - temp_device_ids = set() + # Verify each sensor has a unique name + temp_names = set() temp_object_ids = set() for sensor in temp_sensors: - temp_device_ids.add(sensor.device_id) + temp_names.add(sensor.name) temp_object_ids.add(sensor.object_id) - # All should have object_id "temperature" (no suffix) - assert sensor.object_id == "temperature", ( - f"Expected object_id 'temperature', got '{sensor.object_id}'" - ) - - # Should have 4 different device IDs (including None for main device) - assert len(temp_device_ids) == 4, ( - f"Temperature sensors should be on different devices, got {temp_device_ids}" + # Should have 4 unique names + assert len(temp_names) == 4, ( + f"Temperature sensors should have unique names, got {temp_names}" ) - # Scenario 2: Check binary sensors "Status" on different devices - status_binary = [b for b in binary_sensors if b.name == "Status"] + # Object IDs should also be unique + assert len(temp_object_ids) == 4, ( + f"Temperature sensors should have unique object_ids, got {temp_object_ids}" + ) + + # Scenario 2: Check binary sensors have unique names + status_binary = [b for b in binary_sensors if "Status" in b.name] assert len(status_binary) == 3, ( f"Expected exactly 3 status binary sensors, got {len(status_binary)}" ) - # All should have object_id "status" + # All should have unique object_ids + status_names = set() for binary in status_binary: - assert binary.object_id == "status", ( - f"Expected object_id 'status', got '{binary.object_id}'" - ) + status_names.add(binary.name) + + assert len(status_names) == 3, ( + f"Status binary sensors should have unique names, got {status_names}" + ) # Scenario 3: Check that sensor and binary_sensor can have same name temp_binary = [b for b in binary_sensors if b.name == "Temperature"] @@ -96,62 +100,86 @@ async def test_duplicate_entities_on_different_devices( ) assert temp_binary[0].object_id == "temperature" - # Scenario 4: Check text sensors "Device Info" on different devices - info_text = [t for t in text_sensors if t.name == "Device Info"] + # Scenario 4: Check text sensors have unique names + info_text = [t for t in text_sensors if "Device Info" in t.name] assert len(info_text) == 3, ( f"Expected exactly 3 device info text sensors, got {len(info_text)}" ) - # All should have object_id "device_info" + # All should have unique names and object_ids + info_names = set() for text in info_text: - assert text.object_id == "device_info", ( - f"Expected object_id 'device_info', got '{text.object_id}'" - ) + info_names.add(text.name) - # Scenario 5: Check switches "Power" on different devices - power_switches = [s for s in switches if s.name == "Power"] - assert len(power_switches) == 3, ( - f"Expected exactly 3 power switches, got {len(power_switches)}" + assert len(info_names) == 3, ( + f"Device info text sensors should have unique names, got {info_names}" ) - # All should have object_id "power" - for switch in power_switches: - assert switch.object_id == "power", ( - f"Expected object_id 'power', got '{switch.object_id}'" - ) + # Scenario 5: Check switches have unique names + power_switches = [s for s in switches if "Power" in s.name] + assert len(power_switches) == 4, ( + f"Expected exactly 4 power switches, got {len(power_switches)}" + ) - # Scenario 6: Check empty name buttons (should use device name) - empty_buttons = [b for b in buttons if b.name == ""] - assert len(empty_buttons) == 3, ( - f"Expected exactly 3 empty name buttons, got {len(empty_buttons)}" + # All should have unique names + power_names = set() + for switch in power_switches: + power_names.add(switch.name) + + assert len(power_names) == 4, ( + f"Power switches should have unique names, got {power_names}" + ) + + # Scenario 6: Check reset buttons have unique names + reset_buttons = [b for b in buttons if "Reset" in b.name] + assert len(reset_buttons) == 3, ( + f"Expected exactly 3 reset buttons, got {len(reset_buttons)}" + ) + + # All should have unique names + reset_names = set() + for button in reset_buttons: + reset_names.add(button.name) + + assert len(reset_names) == 3, ( + f"Reset buttons should have unique names, got {reset_names}" + ) + + # Scenario 7: Check empty name selects (should use device names) + empty_selects = [s for s in selects if s.name == ""] + assert len(empty_selects) == 3, ( + f"Expected exactly 3 empty name selects, got {len(empty_selects)}" ) # Group by device - c1_buttons = [b for b in empty_buttons if b.device_id == controller_1.device_id] - c2_buttons = [b for b in empty_buttons if b.device_id == controller_2.device_id] + c1_selects = [s for s in empty_selects if s.device_id == controller_1.device_id] + c2_selects = [s for s in empty_selects if s.device_id == controller_2.device_id] # For main device, device_id is 0 - main_buttons = [b for b in empty_buttons if b.device_id == 0] + main_selects = [s for s in empty_selects if s.device_id == 0] - # Check object IDs for empty name entities - assert len(c1_buttons) == 1 and c1_buttons[0].object_id == "controller_1" - assert len(c2_buttons) == 1 and c2_buttons[0].object_id == "controller_2" + # Check object IDs for empty name entities - they should use device names + assert len(c1_selects) == 1 and c1_selects[0].object_id == "controller_1" + assert len(c2_selects) == 1 and c2_selects[0].object_id == "controller_2" assert ( - len(main_buttons) == 1 - and main_buttons[0].object_id == "duplicate-entities-test" + len(main_selects) == 1 + and main_selects[0].object_id == "duplicate-entities-test" ) - # Scenario 7: Check special characters in number names - temp_numbers = [n for n in numbers if n.name == "Temperature Setpoint!"] + # Scenario 8: Check special characters in number names - now unique + temp_numbers = [n for n in numbers if "Temperature Setpoint!" in n.name] assert len(temp_numbers) == 2, ( f"Expected exactly 2 temperature setpoint numbers, got {len(temp_numbers)}" ) - # Special characters should be sanitized to _ in object_id + # Should have unique names + setpoint_names = set() for number in temp_numbers: - assert number.object_id == "temperature_setpoint_", ( - f"Expected object_id 'temperature_setpoint_', got '{number.object_id}'" - ) + setpoint_names.add(number.name) + + assert len(setpoint_names) == 2, ( + f"Temperature setpoint numbers should have unique names, got {setpoint_names}" + ) # Verify we can get states for all entities (ensures they're functional) loop = asyncio.get_running_loop() @@ -164,6 +192,7 @@ async def test_duplicate_entities_on_different_devices( + len(switches) + len(buttons) + len(numbers) + + len(selects) ) def on_state(state) -> None: diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index e166eeedee..0dcdd84507 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -505,13 +505,13 @@ def test_entity_duplicate_validator() -> None: config1 = {CONF_NAME: "Temperature"} validated1 = validator(config1) assert validated1 == config1 - assert ("", "sensor", "temperature") in CORE.unique_ids + assert ("sensor", "temperature") in CORE.unique_ids # Second entity with different name should pass config2 = {CONF_NAME: "Humidity"} validated2 = validator(config2) assert validated2 == config2 - assert ("", "sensor", "humidity") in CORE.unique_ids + assert ("sensor", "humidity") in CORE.unique_ids # Duplicate entity should fail config3 = {CONF_NAME: "Temperature"} @@ -535,24 +535,36 @@ def test_entity_duplicate_validator_with_devices() -> None: device1 = ID("device1", type="Device") device2 = ID("device2", type="Device") - # Same name on different devices should pass + # First entity on device1 should pass config1 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1} validated1 = validator(config1) assert validated1 == config1 - assert ("device1", "sensor", "temperature") in CORE.unique_ids + assert ("sensor", "temperature") in CORE.unique_ids + # Same name on different device should now fail config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device2} - validated2 = validator(config2) - assert validated2 == config2 - assert ("device2", "sensor", "temperature") in CORE.unique_ids - - # Duplicate on same device should fail - config3 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1} with pytest.raises( Invalid, - match=r"Duplicate sensor entity with name 'Temperature' found on device 'device1'", + match=r"Duplicate sensor entity with name 'Temperature' found. Each entity must have a unique name within its platform across all devices.", ): - validator(config3) + validator(config2) + + # Different name on device2 should pass + config3 = {CONF_NAME: "Humidity", CONF_DEVICE_ID: device2} + validated3 = validator(config3) + assert validated3 == config3 + assert ("sensor", "humidity") in CORE.unique_ids + + # Empty names should use device names and be allowed + config4 = {CONF_NAME: "", CONF_DEVICE_ID: device1} + validated4 = validator(config4) + assert validated4 == config4 + assert ("sensor", "device1") in CORE.unique_ids + + config5 = {CONF_NAME: "", CONF_DEVICE_ID: device2} + validated5 = validator(config5) + assert validated5 == config5 + assert ("sensor", "device2") in CORE.unique_ids def test_duplicate_entity_yaml_validation( @@ -576,10 +588,10 @@ def test_duplicate_entity_with_devices_yaml_validation( ) assert result is None - # Check for the duplicate entity error message with device + # Check for the duplicate entity error message captured = capsys.readouterr() assert ( - "Duplicate sensor entity with name 'Temperature' found on device 'device1'" + "Duplicate sensor entity with name 'Temperature' found. Each entity must have a unique name within its platform across all devices." in captured.out ) From 27c745d5a10d7b7768619ceb9143bf61ac06ae5f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 1 Jul 2025 14:38:39 +1200 Subject: [PATCH 192/293] [host] Disable platformio ldf (#9277) --- esphome/components/host/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index da75873eaf..d3dbcba6ed 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -44,3 +44,4 @@ async def to_code(config): cg.add_build_flag("-std=gnu++20") cg.add_define("ESPHOME_BOARD", "host") cg.add_platformio_option("platform", "platformio/native") + cg.add_platformio_option("lib_ldf_mode", "off") From 8c34b72b62f69d850a54b0ec511f8b7638352776 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Tue, 1 Jul 2025 04:57:00 +0200 Subject: [PATCH 193/293] Jinja expressions in configs (Take #3) (#8955) --- esphome/components/packages/__init__.py | 3 +- esphome/components/substitutions/__init__.py | 95 ++++++++++--- esphome/components/substitutions/jinja.py | 99 ++++++++++++++ esphome/config.py | 1 - esphome/yaml_util.py | 2 - requirements.txt | 1 + .../fixtures/substitutions/.gitignore | 1 + .../substitutions/00-simple_var.approved.yaml | 19 +++ .../substitutions/00-simple_var.input.yaml | 21 +++ .../substitutions/01-include.approved.yaml | 15 +++ .../substitutions/01-include.input.yaml | 15 +++ .../02-expressions.approved.yaml | 24 ++++ .../substitutions/02-expressions.input.yaml | 22 +++ .../substitutions/03-closures.approved.yaml | 17 +++ .../substitutions/03-closures.input.yaml | 16 +++ .../04-display_example.approved.yaml | 5 + .../04-display_example.input.yaml | 7 + .../substitutions/closures_package.yaml | 3 + .../fixtures/substitutions/display.yaml | 11 ++ .../fixtures/substitutions/inc1.yaml | 8 ++ tests/unit_tests/test_substitutions.py | 125 ++++++++++++++++++ 21 files changed, 486 insertions(+), 24 deletions(-) create mode 100644 esphome/components/substitutions/jinja.py create mode 100644 tests/unit_tests/fixtures/substitutions/.gitignore create mode 100644 tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/01-include.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/01-include.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/03-closures.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/03-closures.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/04-display_example.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/04-display_example.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/closures_package.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/display.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/inc1.yaml create mode 100644 tests/unit_tests/test_substitutions.py diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 08ae798282..6eb746ec63 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -74,7 +74,7 @@ BASE_SCHEMA = cv.All( { cv.Required(CONF_PATH): validate_yaml_filename, cv.Optional(CONF_VARS, default={}): cv.Schema( - {cv.string: cv.string} + {cv.string: object} ), } ), @@ -148,7 +148,6 @@ def _process_base_package(config: dict) -> dict: raise cv.Invalid( f"Current ESPHome Version is too old to use this package: {ESPHOME_VERSION} < {min_version}" ) - vars = {k: str(v) for k, v in vars.items()} new_yaml = yaml_util.substitute_vars(new_yaml, vars) packages[f"{filename}{idx}"] = new_yaml except EsphomeError as e: diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 41e49f70db..5878af43b2 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -5,6 +5,13 @@ from esphome.config_helpers import Extend, Remove, merge_config import esphome.config_validation as cv from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS from esphome.yaml_util import ESPHomeDataBase, make_data_base +from .jinja import ( + Jinja, + JinjaStr, + has_jinja, + TemplateError, + TemplateRuntimeError, +) CODEOWNERS = ["@esphome/core"] _LOGGER = logging.getLogger(__name__) @@ -28,7 +35,7 @@ def validate_substitution_key(value): CONFIG_SCHEMA = cv.Schema( { - validate_substitution_key: cv.string_strict, + validate_substitution_key: object, } ) @@ -37,7 +44,42 @@ async def to_code(config): pass -def _expand_substitutions(substitutions, value, path, ignore_missing): +def _expand_jinja(value, orig_value, path, jinja, ignore_missing): + if has_jinja(value): + # If the original value passed in to this function is a JinjaStr, it means it contains an unresolved + # Jinja expression from a previous pass. + if isinstance(orig_value, JinjaStr): + # Rebuild the JinjaStr in case it was lost while replacing substitutions. + value = JinjaStr(value, orig_value.upvalues) + try: + # Invoke the jinja engine to evaluate the expression. + value, err = jinja.expand(value) + if err is not None: + if not ignore_missing and "password" not in path: + _LOGGER.warning( + "Found '%s' (see %s) which looks like an expression," + " but could not resolve all the variables: %s", + value, + "->".join(str(x) for x in path), + err.message, + ) + except ( + TemplateError, + TemplateRuntimeError, + RuntimeError, + ArithmeticError, + AttributeError, + TypeError, + ) as err: + raise cv.Invalid( + f"{type(err).__name__} Error evaluating jinja expression '{value}': {str(err)}." + f" See {'->'.join(str(x) for x in path)}", + path, + ) + return value + + +def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): if "$" not in value: return value @@ -47,7 +89,8 @@ def _expand_substitutions(substitutions, value, path, ignore_missing): while True: m = cv.VARIABLE_PROG.search(value, i) if not m: - # Nothing more to match. Done + # No more variable substitutions found. See if the remainder looks like a jinja template + value = _expand_jinja(value, orig_value, path, jinja, ignore_missing) break i, j = m.span(0) @@ -67,8 +110,15 @@ def _expand_substitutions(substitutions, value, path, ignore_missing): continue sub = substitutions[name] + + if i == 0 and j == len(value): + # The variable spans the whole expression, e.g., "${varName}". Return its resolved value directly + # to conserve its type. + value = sub + break + tail = value[j:] - value = value[:i] + sub + value = value[:i] + str(sub) i = len(value) value += tail @@ -77,36 +127,40 @@ def _expand_substitutions(substitutions, value, path, ignore_missing): if isinstance(orig_value, ESPHomeDataBase): # even though string can get larger or smaller, the range should point # to original document marks - return make_data_base(value, orig_value) + value = make_data_base(value, orig_value) return value -def _substitute_item(substitutions, item, path, ignore_missing): +def _substitute_item(substitutions, item, path, jinja, ignore_missing): if isinstance(item, list): for i, it in enumerate(item): - sub = _substitute_item(substitutions, it, path + [i], ignore_missing) + sub = _substitute_item(substitutions, it, path + [i], jinja, ignore_missing) if sub is not None: item[i] = sub elif isinstance(item, dict): replace_keys = [] for k, v in item.items(): if path or k != CONF_SUBSTITUTIONS: - sub = _substitute_item(substitutions, k, path + [k], ignore_missing) + sub = _substitute_item( + substitutions, k, path + [k], jinja, ignore_missing + ) if sub is not None: replace_keys.append((k, sub)) - sub = _substitute_item(substitutions, v, path + [k], ignore_missing) + sub = _substitute_item(substitutions, v, path + [k], jinja, ignore_missing) if sub is not None: item[k] = sub for old, new in replace_keys: item[new] = merge_config(item.get(old), item.get(new)) del item[old] elif isinstance(item, str): - sub = _expand_substitutions(substitutions, item, path, ignore_missing) - if sub != item: + sub = _expand_substitutions(substitutions, item, path, jinja, ignore_missing) + if isinstance(sub, JinjaStr) or sub != item: return sub elif isinstance(item, (core.Lambda, Extend, Remove)): - sub = _expand_substitutions(substitutions, item.value, path, ignore_missing) + sub = _expand_substitutions( + substitutions, item.value, path, jinja, ignore_missing + ) if sub != item: item.value = sub return None @@ -116,11 +170,11 @@ def do_substitution_pass(config, command_line_substitutions, ignore_missing=Fals if CONF_SUBSTITUTIONS not in config and not command_line_substitutions: return - substitutions = config.get(CONF_SUBSTITUTIONS) - if substitutions is None: - substitutions = command_line_substitutions - elif command_line_substitutions: - substitutions = {**substitutions, **command_line_substitutions} + # Merge substitutions in config, overriding with substitutions coming from command line: + substitutions = { + **config.get(CONF_SUBSTITUTIONS, {}), + **(command_line_substitutions or {}), + } with cv.prepend_path("substitutions"): if not isinstance(substitutions, dict): raise cv.Invalid( @@ -133,7 +187,7 @@ def do_substitution_pass(config, command_line_substitutions, ignore_missing=Fals sub = validate_substitution_key(key) if sub != key: replace_keys.append((key, sub)) - substitutions[key] = cv.string_strict(value) + substitutions[key] = value for old, new in replace_keys: substitutions[new] = substitutions[old] del substitutions[old] @@ -141,4 +195,7 @@ def do_substitution_pass(config, command_line_substitutions, ignore_missing=Fals config[CONF_SUBSTITUTIONS] = substitutions # Move substitutions to the first place to replace substitutions in them correctly config.move_to_end(CONF_SUBSTITUTIONS, False) - _substitute_item(substitutions, config, [], ignore_missing) + + # Create a Jinja environment that will consider substitutions in scope: + jinja = Jinja(substitutions) + _substitute_item(substitutions, config, [], jinja, ignore_missing) diff --git a/esphome/components/substitutions/jinja.py b/esphome/components/substitutions/jinja.py new file mode 100644 index 0000000000..9ecdbab844 --- /dev/null +++ b/esphome/components/substitutions/jinja.py @@ -0,0 +1,99 @@ +import logging +import math +import re +import jinja2 as jinja +from jinja2.nativetypes import NativeEnvironment + +TemplateError = jinja.TemplateError +TemplateSyntaxError = jinja.TemplateSyntaxError +TemplateRuntimeError = jinja.TemplateRuntimeError +UndefinedError = jinja.UndefinedError +Undefined = jinja.Undefined + +_LOGGER = logging.getLogger(__name__) + +DETECT_JINJA = r"(\$\{)" +detect_jinja_re = re.compile( + r"<%.+?%>" # Block form expression: <% ... %> + r"|\$\{[^}]+\}", # Braced form expression: ${ ... } + flags=re.MULTILINE, +) + + +def has_jinja(st): + return detect_jinja_re.search(st) is not None + + +class JinjaStr(str): + """ + Wraps a string containing an unresolved Jinja expression, + storing the variables visible to it when it failed to resolve. + For example, an expression inside a package, `${ A * B }` may fail + to resolve at package parsing time if `A` is a local package var + but `B` is a substitution defined in the root yaml. + Therefore, we store the value of `A` as an upvalue bound + to the original string so we may be able to resolve `${ A * B }` + later in the main substitutions pass. + """ + + def __new__(cls, value: str, upvalues=None): + obj = super().__new__(cls, value) + obj.upvalues = upvalues or {} + return obj + + def __init__(self, value: str, upvalues=None): + self.upvalues = upvalues or {} + + +class Jinja: + """ + Wraps a Jinja environment + """ + + def __init__(self, context_vars): + self.env = NativeEnvironment( + trim_blocks=True, + lstrip_blocks=True, + block_start_string="<%", + block_end_string="%>", + line_statement_prefix="#", + line_comment_prefix="##", + variable_start_string="${", + variable_end_string="}", + undefined=jinja.StrictUndefined, + ) + self.env.add_extension("jinja2.ext.do") + self.env.globals["math"] = math # Inject entire math module + self.context_vars = {**context_vars} + self.env.globals = {**self.env.globals, **self.context_vars} + + def expand(self, content_str): + """ + Renders a string that may contain Jinja expressions or statements + Returns the resulting processed string if all values could be resolved. + Otherwise, it returns a tagged (JinjaStr) string that captures variables + in scope (upvalues), like a closure for later evaluation. + """ + result = None + override_vars = {} + if isinstance(content_str, JinjaStr): + # If `value` is already a JinjaStr, it means we are trying to evaluate it again + # in a parent pass. + # Hopefully, all required variables are visible now. + override_vars = content_str.upvalues + try: + template = self.env.from_string(content_str) + result = template.render(override_vars) + if isinstance(result, Undefined): + # This happens when the expression is simply an undefined variable. Jinja does not + # raise an exception, instead we get "Undefined". + # Trigger an UndefinedError exception so we skip to below. + print("" + result) + except (TemplateSyntaxError, UndefinedError) as err: + # `content_str` contains a Jinja expression that refers to a variable that is undefined + # in this scope. Perhaps it refers to a root substitution that is not visible yet. + # Therefore, return the original `content_str` as a JinjaStr, which contains the variables + # that are actually visible to it at this point to postpone evaluation. + return JinjaStr(content_str, {**self.context_vars, **override_vars}), err + + return result, None diff --git a/esphome/config.py b/esphome/config.py index ca3686a0e6..73cc7657cc 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -789,7 +789,6 @@ def validate_config( result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS) try: substitutions.do_substitution_pass(config, command_line_substitutions) - substitutions.do_substitution_pass(config, command_line_substitutions) except vol.Invalid as err: result.add_error(err) return result diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index bd1806affc..e52fc9e788 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -292,8 +292,6 @@ class ESPHomeLoaderMixin: if file is None: raise yaml.MarkedYAMLError("Must include 'file'", node.start_mark) vars = fields.get(CONF_VARS) - if vars: - vars = {k: str(v) for k, v in vars.items()} return file, vars if isinstance(node, yaml.nodes.MappingNode): diff --git a/requirements.txt b/requirements.txt index 12f3b84359..1010a311d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,6 +21,7 @@ esphome-glyphsets==0.2.0 pillow==10.4.0 cairosvg==2.8.2 freetype-py==2.5.1 +jinja2==3.1.6 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 diff --git a/tests/unit_tests/fixtures/substitutions/.gitignore b/tests/unit_tests/fixtures/substitutions/.gitignore new file mode 100644 index 0000000000..0b15cdb2b7 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/.gitignore @@ -0,0 +1 @@ +*.received.yaml \ No newline at end of file diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml new file mode 100644 index 0000000000..c031399c37 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml @@ -0,0 +1,19 @@ +substitutions: + var1: '1' + var2: '2' + var21: '79' +esphome: + name: test +test_list: + - '1' + - '1' + - '1' + - '1' + - 'Values: 1 2' + - 'Value: 79' + - 1 + 2 + - 1 * 2 + - 'Undefined var: ${undefined_var}' + - ${undefined_var} + - $undefined_var + - ${ undefined_var } diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml new file mode 100644 index 0000000000..88a4ffb991 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml @@ -0,0 +1,21 @@ +esphome: + name: test + +substitutions: + var1: "1" + var2: "2" + var21: "79" + +test_list: + - "$var1" + - "${var1}" + - $var1 + - ${var1} + - "Values: $var1 ${var2}" + - "Value: ${var2${var1}}" + - "$var1 + $var2" + - "${ var1 } * ${ var2 }" + - "Undefined var: ${undefined_var}" + - ${undefined_var} + - $undefined_var + - ${ undefined_var } diff --git a/tests/unit_tests/fixtures/substitutions/01-include.approved.yaml b/tests/unit_tests/fixtures/substitutions/01-include.approved.yaml new file mode 100644 index 0000000000..a812fedcfd --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/01-include.approved.yaml @@ -0,0 +1,15 @@ +substitutions: + var1: '1' + var2: '2' + a: alpha +test_list: + - values: + - var1: '1' + - a: A + - b: B-default + - c: The value of C is C + - values: + - var1: '1' + - a: alpha + - b: beta + - c: The value of C is $c diff --git a/tests/unit_tests/fixtures/substitutions/01-include.input.yaml b/tests/unit_tests/fixtures/substitutions/01-include.input.yaml new file mode 100644 index 0000000000..d3daa681a4 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/01-include.input.yaml @@ -0,0 +1,15 @@ +substitutions: + var1: "1" + var2: "2" + a: "alpha" + +test_list: + - !include + file: inc1.yaml + vars: + a: "A" + c: "C" + - !include + file: inc1.yaml + vars: + b: "beta" diff --git a/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml b/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml new file mode 100644 index 0000000000..9e401ec5d6 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml @@ -0,0 +1,24 @@ +substitutions: + width: 7 + height: 8 + enabled: true + pin: &id001 + number: 18 + inverted: true + area: 25 + numberOne: 1 + var1: 79 +test_list: + - The area is 56 + - 56 + - 56 + 1 + - ENABLED + - list: + - 7 + - 8 + - width: 7 + height: 8 + - *id001 + - The pin number is 18 + - The square root is: 5.0 + - The number is 80 diff --git a/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml b/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml new file mode 100644 index 0000000000..1777b46f67 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml @@ -0,0 +1,22 @@ +substitutions: + width: 7 + height: 8 + enabled: true + pin: + number: 18 + inverted: true + area: 25 + numberOne: 1 + var1: 79 + +test_list: + - "The area is ${width * height}" + - ${width * height} + - ${width * height} + 1 + - ${enabled and "ENABLED" or "DISABLED"} + - list: ${ [width, height] } + - "${ {'width': width, 'height': height} }" + - ${pin} + - The pin number is ${pin.number} + - The square root is: ${math.sqrt(area)} + - The number is ${var${numberOne} + 1} diff --git a/tests/unit_tests/fixtures/substitutions/03-closures.approved.yaml b/tests/unit_tests/fixtures/substitutions/03-closures.approved.yaml new file mode 100644 index 0000000000..c8f7d9976c --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/03-closures.approved.yaml @@ -0,0 +1,17 @@ +substitutions: + B: 5 + var7: 79 +package_result: + - The value of A*B is 35, where A is a package var and B is a substitution in the + root file + - Double substitution also works; the value of var7 is 79, where A is a package + var +local_results: + - The value of B is 5 + - 'You will see, however, that + + ${A} is not substituted here, since + + it is out of scope. + + ' diff --git a/tests/unit_tests/fixtures/substitutions/03-closures.input.yaml b/tests/unit_tests/fixtures/substitutions/03-closures.input.yaml new file mode 100644 index 0000000000..e0b2c39e52 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/03-closures.input.yaml @@ -0,0 +1,16 @@ +substitutions: + B: 5 + var7: 79 + +packages: + closures_package: !include + file: closures_package.yaml + vars: + A: 7 + +local_results: + - The value of B is ${B} + - | + You will see, however, that + ${A} is not substituted here, since + it is out of scope. diff --git a/tests/unit_tests/fixtures/substitutions/04-display_example.approved.yaml b/tests/unit_tests/fixtures/substitutions/04-display_example.approved.yaml new file mode 100644 index 0000000000..f559181b45 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/04-display_example.approved.yaml @@ -0,0 +1,5 @@ +display: + - platform: ili9xxx + dimensions: + width: 960 + height: 544 diff --git a/tests/unit_tests/fixtures/substitutions/04-display_example.input.yaml b/tests/unit_tests/fixtures/substitutions/04-display_example.input.yaml new file mode 100644 index 0000000000..9d8f64a253 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/04-display_example.input.yaml @@ -0,0 +1,7 @@ +# main.yaml +packages: + my_display: !include + file: display.yaml + vars: + high_dpi: true + native_height: 272 diff --git a/tests/unit_tests/fixtures/substitutions/closures_package.yaml b/tests/unit_tests/fixtures/substitutions/closures_package.yaml new file mode 100644 index 0000000000..e87908814d --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/closures_package.yaml @@ -0,0 +1,3 @@ +package_result: + - The value of A*B is ${A * B}, where A is a package var and B is a substitution in the root file + - Double substitution also works; the value of var7 is ${var$A}, where A is a package var diff --git a/tests/unit_tests/fixtures/substitutions/display.yaml b/tests/unit_tests/fixtures/substitutions/display.yaml new file mode 100644 index 0000000000..1e2249dddb --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/display.yaml @@ -0,0 +1,11 @@ +# display.yaml + +defaults: + native_width: 480 + native_height: 480 + +display: + - platform: ili9xxx + dimensions: + width: ${high_dpi and native_width * 2 or native_width} + height: ${high_dpi and native_height * 2 or native_height} diff --git a/tests/unit_tests/fixtures/substitutions/inc1.yaml b/tests/unit_tests/fixtures/substitutions/inc1.yaml new file mode 100644 index 0000000000..65b91a5e16 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/inc1.yaml @@ -0,0 +1,8 @@ +defaults: + b: "B-default" + +values: + - var1: $var1 + - a: $a + - b: ${b} + - c: The value of C is $c diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py new file mode 100644 index 0000000000..b377499d29 --- /dev/null +++ b/tests/unit_tests/test_substitutions.py @@ -0,0 +1,125 @@ +import glob +import logging +import os + +from esphome import yaml_util +from esphome.components import substitutions +from esphome.const import CONF_PACKAGES + +_LOGGER = logging.getLogger(__name__) + +# Set to True for dev mode behavior +# This will generate the expected version of the test files. + +DEV_MODE = False + + +def sort_dicts(obj): + """Recursively sort dictionaries for order-insensitive comparison.""" + if isinstance(obj, dict): + return {k: sort_dicts(obj[k]) for k in sorted(obj)} + elif isinstance(obj, list): + # Lists are not sorted; we preserve order + return [sort_dicts(i) for i in obj] + else: + return obj + + +def dict_diff(a, b, path=""): + """Recursively find differences between two dict/list structures.""" + diffs = [] + if isinstance(a, dict) and isinstance(b, dict): + a_keys = set(a) + b_keys = set(b) + for key in a_keys - b_keys: + diffs.append(f"{path}/{key} only in actual") + for key in b_keys - a_keys: + diffs.append(f"{path}/{key} only in expected") + for key in a_keys & b_keys: + diffs.extend(dict_diff(a[key], b[key], f"{path}/{key}")) + elif isinstance(a, list) and isinstance(b, list): + min_len = min(len(a), len(b)) + for i in range(min_len): + diffs.extend(dict_diff(a[i], b[i], f"{path}[{i}]")) + if len(a) > len(b): + for i in range(min_len, len(a)): + diffs.append(f"{path}[{i}] only in actual: {a[i]!r}") + elif len(b) > len(a): + for i in range(min_len, len(b)): + diffs.append(f"{path}[{i}] only in expected: {b[i]!r}") + else: + if a != b: + diffs.append(f"\t{path}: actual={a!r} expected={b!r}") + return diffs + + +def write_yaml(path, data): + with open(path, "w", encoding="utf-8") as f: + f.write(yaml_util.dump(data)) + + +def test_substitutions_fixtures(fixture_path): + base_dir = fixture_path / "substitutions" + sources = sorted(glob.glob(str(base_dir / "*.input.yaml"))) + assert sources, f"No input YAML files found in {base_dir}" + + failures = [] + for source_path in sources: + try: + expected_path = source_path.replace(".input.yaml", ".approved.yaml") + test_case = os.path.splitext(os.path.basename(source_path))[0].replace( + ".input", "" + ) + + # Load using ESPHome's YAML loader + config = yaml_util.load_yaml(source_path) + + if CONF_PACKAGES in config: + from esphome.components.packages import do_packages_pass + + config = do_packages_pass(config) + + substitutions.do_substitution_pass(config, None) + + # Also load expected using ESPHome's loader, or use {} if missing and DEV_MODE + if os.path.isfile(expected_path): + expected = yaml_util.load_yaml(expected_path) + elif DEV_MODE: + expected = {} + else: + assert os.path.isfile(expected_path), ( + f"Expected file missing: {expected_path}" + ) + + # Sort dicts only (not lists) for comparison + got_sorted = sort_dicts(config) + expected_sorted = sort_dicts(expected) + + if got_sorted != expected_sorted: + diff = "\n".join(dict_diff(got_sorted, expected_sorted)) + msg = ( + f"Substitution result mismatch for {os.path.basename(source_path)}\n" + f"Diff:\n{diff}\n\n" + f"Got: {got_sorted}\n" + f"Expected: {expected_sorted}" + ) + # Write out the received file when test fails + if DEV_MODE: + received_path = os.path.join( + os.path.dirname(source_path), f"{test_case}.received.yaml" + ) + write_yaml(received_path, config) + print(msg) + failures.append(msg) + else: + raise AssertionError(msg) + except Exception as err: + _LOGGER.error("Error in test file %s", source_path) + raise err + + if DEV_MODE and failures: + print(f"\n{len(failures)} substitution test case(s) failed.") + + if DEV_MODE: + _LOGGER.error("Tests passed, but Dev mode is enabled.") + assert not DEV_MODE # make sure DEV_MODE is disabled after you are finished. From e3ccb9b46c9b0c5535d9d3c5bbdefe03b0c1d7b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 Jun 2025 22:04:50 -0500 Subject: [PATCH 194/293] Use interrupt based approach for esp32_touch (#9059) Co-authored-by: Keith Burzinski --- .../components/esp32_touch/esp32_touch.cpp | 355 ---------------- esphome/components/esp32_touch/esp32_touch.h | 209 +++++++-- .../esp32_touch/esp32_touch_common.cpp | 159 +++++++ .../components/esp32_touch/esp32_touch_v1.cpp | 240 +++++++++++ .../components/esp32_touch/esp32_touch_v2.cpp | 398 ++++++++++++++++++ 5 files changed, 975 insertions(+), 386 deletions(-) delete mode 100644 esphome/components/esp32_touch/esp32_touch.cpp create mode 100644 esphome/components/esp32_touch/esp32_touch_common.cpp create mode 100644 esphome/components/esp32_touch/esp32_touch_v1.cpp create mode 100644 esphome/components/esp32_touch/esp32_touch_v2.cpp diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp deleted file mode 100644 index 366aa10697..0000000000 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ /dev/null @@ -1,355 +0,0 @@ -#ifdef USE_ESP32 - -#include "esp32_touch.h" -#include "esphome/core/application.h" -#include "esphome/core/log.h" -#include "esphome/core/hal.h" - -#include - -namespace esphome { -namespace esp32_touch { - -static const char *const TAG = "esp32_touch"; - -void ESP32TouchComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - touch_pad_init(); -// set up and enable/start filtering based on ESP32 variant -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - if (this->filter_configured_()) { - touch_filter_config_t filter_info = { - .mode = this->filter_mode_, - .debounce_cnt = this->debounce_count_, - .noise_thr = this->noise_threshold_, - .jitter_step = this->jitter_step_, - .smh_lvl = this->smooth_level_, - }; - touch_pad_filter_set_config(&filter_info); - touch_pad_filter_enable(); - } - - if (this->denoise_configured_()) { - touch_pad_denoise_t denoise = { - .grade = this->grade_, - .cap_level = this->cap_level_, - }; - touch_pad_denoise_set_config(&denoise); - touch_pad_denoise_enable(); - } - - if (this->waterproof_configured_()) { - touch_pad_waterproof_t waterproof = { - .guard_ring_pad = this->waterproof_guard_ring_pad_, - .shield_driver = this->waterproof_shield_driver_, - }; - touch_pad_waterproof_set_config(&waterproof); - touch_pad_waterproof_enable(); - } -#else - if (this->iir_filter_enabled_()) { - touch_pad_filter_start(this->iir_filter_); - } -#endif - -#if ESP_IDF_VERSION_MAJOR >= 5 && defined(USE_ESP32_VARIANT_ESP32) - touch_pad_set_measurement_clock_cycles(this->meas_cycle_); - touch_pad_set_measurement_interval(this->sleep_cycle_); -#else - touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_); -#endif - touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_); - - for (auto *child : this->children_) { -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - touch_pad_config(child->get_touch_pad()); -#else - // Disable interrupt threshold - touch_pad_config(child->get_touch_pad(), 0); -#endif - } -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); - touch_pad_fsm_start(); -#endif -} - -void ESP32TouchComponent::dump_config() { - ESP_LOGCONFIG(TAG, - "Config for ESP32 Touch Hub:\n" - " Meas cycle: %.2fms\n" - " Sleep cycle: %.2fms", - this->meas_cycle_ / (8000000.0f / 1000.0f), this->sleep_cycle_ / (150000.0f / 1000.0f)); - - const char *lv_s; - switch (this->low_voltage_reference_) { - case TOUCH_LVOLT_0V5: - lv_s = "0.5V"; - break; - case TOUCH_LVOLT_0V6: - lv_s = "0.6V"; - break; - case TOUCH_LVOLT_0V7: - lv_s = "0.7V"; - break; - case TOUCH_LVOLT_0V8: - lv_s = "0.8V"; - break; - default: - lv_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Low Voltage Reference: %s", lv_s); - - const char *hv_s; - switch (this->high_voltage_reference_) { - case TOUCH_HVOLT_2V4: - hv_s = "2.4V"; - break; - case TOUCH_HVOLT_2V5: - hv_s = "2.5V"; - break; - case TOUCH_HVOLT_2V6: - hv_s = "2.6V"; - break; - case TOUCH_HVOLT_2V7: - hv_s = "2.7V"; - break; - default: - hv_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " High Voltage Reference: %s", hv_s); - - const char *atten_s; - switch (this->voltage_attenuation_) { - case TOUCH_HVOLT_ATTEN_1V5: - atten_s = "1.5V"; - break; - case TOUCH_HVOLT_ATTEN_1V: - atten_s = "1V"; - break; - case TOUCH_HVOLT_ATTEN_0V5: - atten_s = "0.5V"; - break; - case TOUCH_HVOLT_ATTEN_0V: - atten_s = "0V"; - break; - default: - atten_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Voltage Attenuation: %s", atten_s); - -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - if (this->filter_configured_()) { - const char *filter_mode_s; - switch (this->filter_mode_) { - case TOUCH_PAD_FILTER_IIR_4: - filter_mode_s = "IIR_4"; - break; - case TOUCH_PAD_FILTER_IIR_8: - filter_mode_s = "IIR_8"; - break; - case TOUCH_PAD_FILTER_IIR_16: - filter_mode_s = "IIR_16"; - break; - case TOUCH_PAD_FILTER_IIR_32: - filter_mode_s = "IIR_32"; - break; - case TOUCH_PAD_FILTER_IIR_64: - filter_mode_s = "IIR_64"; - break; - case TOUCH_PAD_FILTER_IIR_128: - filter_mode_s = "IIR_128"; - break; - case TOUCH_PAD_FILTER_IIR_256: - filter_mode_s = "IIR_256"; - break; - case TOUCH_PAD_FILTER_JITTER: - filter_mode_s = "JITTER"; - break; - default: - filter_mode_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, - " Filter mode: %s\n" - " Debounce count: %" PRIu32 "\n" - " Noise threshold coefficient: %" PRIu32 "\n" - " Jitter filter step size: %" PRIu32, - filter_mode_s, this->debounce_count_, this->noise_threshold_, this->jitter_step_); - const char *smooth_level_s; - switch (this->smooth_level_) { - case TOUCH_PAD_SMOOTH_OFF: - smooth_level_s = "OFF"; - break; - case TOUCH_PAD_SMOOTH_IIR_2: - smooth_level_s = "IIR_2"; - break; - case TOUCH_PAD_SMOOTH_IIR_4: - smooth_level_s = "IIR_4"; - break; - case TOUCH_PAD_SMOOTH_IIR_8: - smooth_level_s = "IIR_8"; - break; - default: - smooth_level_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Smooth level: %s", smooth_level_s); - } - - if (this->denoise_configured_()) { - const char *grade_s; - switch (this->grade_) { - case TOUCH_PAD_DENOISE_BIT12: - grade_s = "BIT12"; - break; - case TOUCH_PAD_DENOISE_BIT10: - grade_s = "BIT10"; - break; - case TOUCH_PAD_DENOISE_BIT8: - grade_s = "BIT8"; - break; - case TOUCH_PAD_DENOISE_BIT4: - grade_s = "BIT4"; - break; - default: - grade_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Denoise grade: %s", grade_s); - - const char *cap_level_s; - switch (this->cap_level_) { - case TOUCH_PAD_DENOISE_CAP_L0: - cap_level_s = "L0"; - break; - case TOUCH_PAD_DENOISE_CAP_L1: - cap_level_s = "L1"; - break; - case TOUCH_PAD_DENOISE_CAP_L2: - cap_level_s = "L2"; - break; - case TOUCH_PAD_DENOISE_CAP_L3: - cap_level_s = "L3"; - break; - case TOUCH_PAD_DENOISE_CAP_L4: - cap_level_s = "L4"; - break; - case TOUCH_PAD_DENOISE_CAP_L5: - cap_level_s = "L5"; - break; - case TOUCH_PAD_DENOISE_CAP_L6: - cap_level_s = "L6"; - break; - case TOUCH_PAD_DENOISE_CAP_L7: - cap_level_s = "L7"; - break; - default: - cap_level_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Denoise capacitance level: %s", cap_level_s); - } -#else - if (this->iir_filter_enabled_()) { - ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_); - } else { - ESP_LOGCONFIG(TAG, " IIR Filter DISABLED"); - } -#endif - - if (this->setup_mode_) { - ESP_LOGCONFIG(TAG, " Setup Mode ENABLED"); - } - - for (auto *child : this->children_) { - LOG_BINARY_SENSOR(" ", "Touch Pad", child); - ESP_LOGCONFIG(TAG, " Pad: T%" PRIu32, (uint32_t) child->get_touch_pad()); - ESP_LOGCONFIG(TAG, " Threshold: %" PRIu32, child->get_threshold()); - } -} - -uint32_t ESP32TouchComponent::component_touch_pad_read(touch_pad_t tp) { -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - uint32_t value = 0; - if (this->filter_configured_()) { - touch_pad_filter_read_smooth(tp, &value); - } else { - touch_pad_read_raw_data(tp, &value); - } -#else - uint16_t value = 0; - if (this->iir_filter_enabled_()) { - touch_pad_read_filtered(tp, &value); - } else { - touch_pad_read(tp, &value); - } -#endif - return value; -} - -void ESP32TouchComponent::loop() { - const uint32_t now = App.get_loop_component_start_time(); - bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250; - for (auto *child : this->children_) { - child->value_ = this->component_touch_pad_read(child->get_touch_pad()); -#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) - child->publish_state(child->value_ < child->get_threshold()); -#else - child->publish_state(child->value_ > child->get_threshold()); -#endif - - if (should_print) { - ESP_LOGD(TAG, "Touch Pad '%s' (T%" PRIu32 "): %" PRIu32, child->get_name().c_str(), - (uint32_t) child->get_touch_pad(), child->value_); - } - - App.feed_wdt(); - } - - if (should_print) { - // Avoid spamming logs - this->setup_mode_last_log_print_ = now; - } -} - -void ESP32TouchComponent::on_shutdown() { - bool is_wakeup_source = false; - -#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) - if (this->iir_filter_enabled_()) { - touch_pad_filter_stop(); - touch_pad_filter_delete(); - } -#endif - - for (auto *child : this->children_) { - if (child->get_wakeup_threshold() != 0) { - if (!is_wakeup_source) { - is_wakeup_source = true; - // Touch sensor FSM mode must be 'TOUCH_FSM_MODE_TIMER' to use it to wake-up. - touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); - } - -#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) - // No filter available when using as wake-up source. - touch_pad_config(child->get_touch_pad(), child->get_wakeup_threshold()); -#endif - } - } - - if (!is_wakeup_source) { - touch_pad_deinit(); - } -} - -ESP32TouchBinarySensor::ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold) - : touch_pad_(touch_pad), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {} - -} // namespace esp32_touch -} // namespace esphome - -#endif diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 3fce8a7e18..576c1a5649 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -9,10 +9,26 @@ #include #include +#include +#include namespace esphome { namespace esp32_touch { +// IMPORTANT: Touch detection logic differs between ESP32 variants: +// - ESP32 v1 (original): Touch detected when value < threshold (capacitance increase causes value decrease) +// - ESP32-S2/S3 v2: Touch detected when value > threshold (capacitance increase causes value increase) +// This inversion is due to different hardware implementations between chip generations. +// +// INTERRUPT BEHAVIOR: +// - ESP32 v1: Interrupts fire when ANY pad is touched and continue while touched. +// Releases are detected by timeout since hardware doesn't generate release interrupts. +// - ESP32-S2/S3 v2: Hardware supports both touch and release interrupts, but release +// interrupts are unreliable and sometimes don't fire. We now only use touch interrupts +// and detect releases via timeout, similar to v1. + +static const uint32_t SETUP_MODE_LOG_INTERVAL_MS = 250; + class ESP32TouchBinarySensor; class ESP32TouchComponent : public Component { @@ -31,6 +47,14 @@ class ESP32TouchComponent : public Component { void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) { this->voltage_attenuation_ = voltage_attenuation; } + + void setup() override; + void dump_config() override; + void loop() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void on_shutdown() override; + #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) void set_filter_mode(touch_filter_mode_t filter_mode) { this->filter_mode_ = filter_mode; } void set_debounce_count(uint32_t debounce_count) { this->debounce_count_ = debounce_count; } @@ -47,16 +71,101 @@ class ESP32TouchComponent : public Component { void set_iir_filter(uint32_t iir_filter) { this->iir_filter_ = iir_filter; } #endif - uint32_t component_touch_pad_read(touch_pad_t tp); + protected: + // Common helper methods + void dump_config_base_(); + void dump_config_sensors_(); + bool create_touch_queue_(); + void cleanup_touch_queue_(); + void configure_wakeup_pads_(); - void setup() override; - void dump_config() override; - void loop() override; + // Helper methods for loop() logic + void process_setup_mode_logging_(uint32_t now); + bool should_check_for_releases_(uint32_t now); + void publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now); + void check_and_disable_loop_if_all_released_(size_t pads_off); + void calculate_release_timeout_(); - void on_shutdown() override; + // Common members + std::vector children_; + bool setup_mode_{false}; + uint32_t setup_mode_last_log_print_{0}; + uint32_t last_release_check_{0}; + uint32_t release_timeout_ms_{1500}; + uint32_t release_check_interval_ms_{50}; + bool initial_state_published_[TOUCH_PAD_MAX] = {false}; + + // Common configuration parameters + uint16_t sleep_cycle_{4095}; + uint16_t meas_cycle_{65535}; + touch_low_volt_t low_voltage_reference_{TOUCH_LVOLT_0V5}; + touch_high_volt_t high_voltage_reference_{TOUCH_HVOLT_2V7}; + touch_volt_atten_t voltage_attenuation_{TOUCH_HVOLT_ATTEN_0V}; + + // Common constants + static constexpr uint32_t MINIMUM_RELEASE_TIME_MS = 100; + + // ==================== PLATFORM SPECIFIC ==================== + +#ifdef USE_ESP32_VARIANT_ESP32 + // ESP32 v1 specific + + static void touch_isr_handler(void *arg); + QueueHandle_t touch_queue_{nullptr}; + + private: + // Touch event structure for ESP32 v1 + // Contains touch pad info, value, and touch state for queue communication + struct TouchPadEventV1 { + touch_pad_t pad; + uint32_t value; + bool is_touched; + }; protected: -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + // Design note: last_touch_time_ does not require synchronization primitives because: + // 1. ESP32 guarantees atomic 32-bit aligned reads/writes + // 2. ISR only writes timestamps, main loop only reads + // 3. Timing tolerance allows for occasional stale reads (50ms check interval) + // 4. Queue operations provide implicit memory barriers + // Using atomic/critical sections would add overhead without meaningful benefit + uint32_t last_touch_time_[TOUCH_PAD_MAX] = {0}; + uint32_t iir_filter_{0}; + + bool iir_filter_enabled_() const { return this->iir_filter_ > 0; } + +#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + // ESP32-S2/S3 v2 specific + static void touch_isr_handler(void *arg); + QueueHandle_t touch_queue_{nullptr}; + + private: + // Touch event structure for ESP32 v2 (S2/S3) + // Contains touch pad and interrupt mask for queue communication + struct TouchPadEventV2 { + touch_pad_t pad; + uint32_t intr_mask; + }; + + // Track last touch time for timeout-based release detection + uint32_t last_touch_time_[TOUCH_PAD_MAX] = {0}; + + protected: + // Filter configuration + touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX}; + uint32_t debounce_count_{0}; + uint32_t noise_threshold_{0}; + uint32_t jitter_step_{0}; + touch_smooth_mode_t smooth_level_{TOUCH_PAD_SMOOTH_MAX}; + + // Denoise configuration + touch_pad_denoise_grade_t grade_{TOUCH_PAD_DENOISE_MAX}; + touch_pad_denoise_cap_t cap_level_{TOUCH_PAD_DENOISE_CAP_MAX}; + + // Waterproof configuration + touch_pad_t waterproof_guard_ring_pad_{TOUCH_PAD_MAX}; + touch_pad_shield_driver_t waterproof_shield_driver_{TOUCH_PAD_SHIELD_DRV_MAX}; + bool filter_configured_() const { return (this->filter_mode_ != TOUCH_PAD_FILTER_MAX) && (this->smooth_level_ != TOUCH_PAD_SMOOTH_MAX); } @@ -67,43 +176,78 @@ class ESP32TouchComponent : public Component { return (this->waterproof_guard_ring_pad_ != TOUCH_PAD_MAX) && (this->waterproof_shield_driver_ != TOUCH_PAD_SHIELD_DRV_MAX); } -#else - bool iir_filter_enabled_() const { return this->iir_filter_ > 0; } + + // Helper method to read touch values - non-blocking operation + // Returns the current touch pad value using either filtered or raw reading + // based on the filter configuration + uint32_t read_touch_value(touch_pad_t pad) const; + + // Helper to update touch state with a known state + void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched); + + // Helper to read touch value and update state for a given child + bool check_and_update_touch_state_(ESP32TouchBinarySensor *child); #endif - std::vector children_; - bool setup_mode_{false}; - uint32_t setup_mode_last_log_print_{0}; - // common parameters - uint16_t sleep_cycle_{4095}; - uint16_t meas_cycle_{65535}; - touch_low_volt_t low_voltage_reference_{TOUCH_LVOLT_0V5}; - touch_high_volt_t high_voltage_reference_{TOUCH_HVOLT_2V7}; - touch_volt_atten_t voltage_attenuation_{TOUCH_HVOLT_ATTEN_0V}; -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX}; - uint32_t debounce_count_{0}; - uint32_t noise_threshold_{0}; - uint32_t jitter_step_{0}; - touch_smooth_mode_t smooth_level_{TOUCH_PAD_SMOOTH_MAX}; - touch_pad_denoise_grade_t grade_{TOUCH_PAD_DENOISE_MAX}; - touch_pad_denoise_cap_t cap_level_{TOUCH_PAD_DENOISE_CAP_MAX}; - touch_pad_t waterproof_guard_ring_pad_{TOUCH_PAD_MAX}; - touch_pad_shield_driver_t waterproof_shield_driver_{TOUCH_PAD_SHIELD_DRV_MAX}; -#else - uint32_t iir_filter_{0}; -#endif + // Helper functions for dump_config - common to both implementations + static const char *get_low_voltage_reference_str(touch_low_volt_t ref) { + switch (ref) { + case TOUCH_LVOLT_0V5: + return "0.5V"; + case TOUCH_LVOLT_0V6: + return "0.6V"; + case TOUCH_LVOLT_0V7: + return "0.7V"; + case TOUCH_LVOLT_0V8: + return "0.8V"; + default: + return "UNKNOWN"; + } + } + + static const char *get_high_voltage_reference_str(touch_high_volt_t ref) { + switch (ref) { + case TOUCH_HVOLT_2V4: + return "2.4V"; + case TOUCH_HVOLT_2V5: + return "2.5V"; + case TOUCH_HVOLT_2V6: + return "2.6V"; + case TOUCH_HVOLT_2V7: + return "2.7V"; + default: + return "UNKNOWN"; + } + } + + static const char *get_voltage_attenuation_str(touch_volt_atten_t atten) { + switch (atten) { + case TOUCH_HVOLT_ATTEN_1V5: + return "1.5V"; + case TOUCH_HVOLT_ATTEN_1V: + return "1V"; + case TOUCH_HVOLT_ATTEN_0V5: + return "0.5V"; + case TOUCH_HVOLT_ATTEN_0V: + return "0V"; + default: + return "UNKNOWN"; + } + } }; /// Simple helper class to expose a touch pad value as a binary sensor. class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { public: - ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold); + ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold) + : touch_pad_(touch_pad), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {} touch_pad_t get_touch_pad() const { return this->touch_pad_; } uint32_t get_threshold() const { return this->threshold_; } void set_threshold(uint32_t threshold) { this->threshold_ = threshold; } +#ifdef USE_ESP32_VARIANT_ESP32 uint32_t get_value() const { return this->value_; } +#endif uint32_t get_wakeup_threshold() const { return this->wakeup_threshold_; } protected: @@ -111,7 +255,10 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { touch_pad_t touch_pad_{TOUCH_PAD_MAX}; uint32_t threshold_{0}; +#ifdef USE_ESP32_VARIANT_ESP32 uint32_t value_{0}; +#endif + bool last_state_{false}; const uint32_t wakeup_threshold_{0}; }; diff --git a/esphome/components/esp32_touch/esp32_touch_common.cpp b/esphome/components/esp32_touch/esp32_touch_common.cpp new file mode 100644 index 0000000000..fd2cdfcbad --- /dev/null +++ b/esphome/components/esp32_touch/esp32_touch_common.cpp @@ -0,0 +1,159 @@ +#ifdef USE_ESP32 + +#include "esp32_touch.h" +#include "esphome/core/log.h" +#include + +#include "soc/rtc.h" + +namespace esphome { +namespace esp32_touch { + +static const char *const TAG = "esp32_touch"; + +void ESP32TouchComponent::dump_config_base_() { + const char *lv_s = get_low_voltage_reference_str(this->low_voltage_reference_); + const char *hv_s = get_high_voltage_reference_str(this->high_voltage_reference_); + const char *atten_s = get_voltage_attenuation_str(this->voltage_attenuation_); + + ESP_LOGCONFIG(TAG, + "Config for ESP32 Touch Hub:\n" + " Meas cycle: %.2fms\n" + " Sleep cycle: %.2fms\n" + " Low Voltage Reference: %s\n" + " High Voltage Reference: %s\n" + " Voltage Attenuation: %s", + this->meas_cycle_ / (8000000.0f / 1000.0f), this->sleep_cycle_ / (150000.0f / 1000.0f), lv_s, hv_s, + atten_s); +} + +void ESP32TouchComponent::dump_config_sensors_() { + for (auto *child : this->children_) { + LOG_BINARY_SENSOR(" ", "Touch Pad", child); + ESP_LOGCONFIG(TAG, " Pad: T%" PRIu32, (uint32_t) child->get_touch_pad()); + ESP_LOGCONFIG(TAG, " Threshold: %" PRIu32, child->get_threshold()); + } +} + +bool ESP32TouchComponent::create_touch_queue_() { + // Queue size calculation: children * 4 allows for burst scenarios where ISR + // fires multiple times before main loop processes. + size_t queue_size = this->children_.size() * 4; + if (queue_size < 8) + queue_size = 8; + +#ifdef USE_ESP32_VARIANT_ESP32 + this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchPadEventV1)); +#else + this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchPadEventV2)); +#endif + + if (this->touch_queue_ == nullptr) { + ESP_LOGE(TAG, "Failed to create touch event queue of size %" PRIu32, (uint32_t) queue_size); + this->mark_failed(); + return false; + } + return true; +} + +void ESP32TouchComponent::cleanup_touch_queue_() { + if (this->touch_queue_) { + vQueueDelete(this->touch_queue_); + this->touch_queue_ = nullptr; + } +} + +void ESP32TouchComponent::configure_wakeup_pads_() { + bool is_wakeup_source = false; + + // Check if any pad is configured for wakeup + for (auto *child : this->children_) { + if (child->get_wakeup_threshold() != 0) { + is_wakeup_source = true; + +#ifdef USE_ESP32_VARIANT_ESP32 + // ESP32 v1: No filter available when using as wake-up source. + touch_pad_config(child->get_touch_pad(), child->get_wakeup_threshold()); +#else + // ESP32-S2/S3 v2: Set threshold for wakeup + touch_pad_set_thresh(child->get_touch_pad(), child->get_wakeup_threshold()); +#endif + } + } + + if (!is_wakeup_source) { + // If no pad is configured for wakeup, deinitialize touch pad + touch_pad_deinit(); + } +} + +void ESP32TouchComponent::process_setup_mode_logging_(uint32_t now) { + if (this->setup_mode_ && now - this->setup_mode_last_log_print_ > SETUP_MODE_LOG_INTERVAL_MS) { + for (auto *child : this->children_) { +#ifdef USE_ESP32_VARIANT_ESP32 + ESP_LOGD(TAG, "Touch Pad '%s' (T%" PRIu32 "): %" PRIu32, child->get_name().c_str(), + (uint32_t) child->get_touch_pad(), child->value_); +#else + // Read the value being used for touch detection + uint32_t value = this->read_touch_value(child->get_touch_pad()); + ESP_LOGD(TAG, "Touch Pad '%s' (T%d): %d", child->get_name().c_str(), child->get_touch_pad(), value); +#endif + } + this->setup_mode_last_log_print_ = now; + } +} + +bool ESP32TouchComponent::should_check_for_releases_(uint32_t now) { + if (now - this->last_release_check_ < this->release_check_interval_ms_) { + return false; + } + this->last_release_check_ = now; + return true; +} + +void ESP32TouchComponent::publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now) { + touch_pad_t pad = child->get_touch_pad(); + if (!this->initial_state_published_[pad]) { + // Check if enough time has passed since startup + if (now > this->release_timeout_ms_) { + child->publish_initial_state(false); + this->initial_state_published_[pad] = true; + ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (initial)", child->get_name().c_str()); + } + } +} + +void ESP32TouchComponent::check_and_disable_loop_if_all_released_(size_t pads_off) { + // Disable the loop to save CPU cycles when all pads are off and not in setup mode. + if (pads_off == this->children_.size() && !this->setup_mode_) { + this->disable_loop(); + } +} + +void ESP32TouchComponent::calculate_release_timeout_() { + // Calculate release timeout based on sleep cycle + // Design note: Hardware limitation - interrupts only fire reliably on touch (not release) + // We must use timeout-based detection for release events + // Formula: 3 sleep cycles converted to ms, with MINIMUM_RELEASE_TIME_MS minimum + // Per ESP-IDF docs: t_sleep = sleep_cycle / SOC_CLK_RC_SLOW_FREQ_APPROX + + uint32_t rtc_freq = rtc_clk_slow_freq_get_hz(); + + // Calculate timeout as 3 sleep cycles + this->release_timeout_ms_ = (this->sleep_cycle_ * 1000 * 3) / rtc_freq; + + if (this->release_timeout_ms_ < MINIMUM_RELEASE_TIME_MS) { + this->release_timeout_ms_ = MINIMUM_RELEASE_TIME_MS; + } + + // Check for releases at 1/4 the timeout interval + // Since hardware doesn't generate reliable release interrupts, we must poll + // for releases in the main loop. Checking at 1/4 the timeout interval provides + // a good balance between responsiveness and efficiency. + this->release_check_interval_ms_ = this->release_timeout_ms_ / 4; +} + +} // namespace esp32_touch +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_touch/esp32_touch_v1.cpp b/esphome/components/esp32_touch/esp32_touch_v1.cpp new file mode 100644 index 0000000000..a6d499e9fa --- /dev/null +++ b/esphome/components/esp32_touch/esp32_touch_v1.cpp @@ -0,0 +1,240 @@ +#ifdef USE_ESP32_VARIANT_ESP32 + +#include "esp32_touch.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +#include +#include + +// Include HAL for ISR-safe touch reading +#include "hal/touch_sensor_ll.h" + +namespace esphome { +namespace esp32_touch { + +static const char *const TAG = "esp32_touch"; + +void ESP32TouchComponent::setup() { + // Create queue for touch events + // Queue size calculation: children * 4 allows for burst scenarios where ISR + // fires multiple times before main loop processes. This is important because + // ESP32 v1 scans all pads on each interrupt, potentially sending multiple events. + if (!this->create_touch_queue_()) { + return; + } + + touch_pad_init(); + touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); + + // Set up IIR filter if enabled + if (this->iir_filter_enabled_()) { + touch_pad_filter_start(this->iir_filter_); + } + + // Configure measurement parameters +#if ESP_IDF_VERSION_MAJOR >= 5 + touch_pad_set_measurement_clock_cycles(this->meas_cycle_); + touch_pad_set_measurement_interval(this->sleep_cycle_); +#else + touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_); +#endif + touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_); + + // Configure each touch pad + for (auto *child : this->children_) { + touch_pad_config(child->get_touch_pad(), child->get_threshold()); + } + + // Register ISR handler + esp_err_t err = touch_pad_isr_register(touch_isr_handler, this); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to register touch ISR: %s", esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + + // Calculate release timeout based on sleep cycle + this->calculate_release_timeout_(); + + // Enable touch pad interrupt + touch_pad_intr_enable(); +} + +void ESP32TouchComponent::dump_config() { + this->dump_config_base_(); + + if (this->iir_filter_enabled_()) { + ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_); + } else { + ESP_LOGCONFIG(TAG, " IIR Filter DISABLED"); + } + + if (this->setup_mode_) { + ESP_LOGCONFIG(TAG, " Setup Mode ENABLED"); + } + + this->dump_config_sensors_(); +} + +void ESP32TouchComponent::loop() { + const uint32_t now = App.get_loop_component_start_time(); + + // Print debug info for all pads in setup mode + this->process_setup_mode_logging_(now); + + // Process any queued touch events from interrupts + // Note: Events are only sent by ISR for pads that were measured in that cycle (value != 0) + // This is more efficient than sending all pad states every interrupt + TouchPadEventV1 event; + while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) { + // Find the corresponding sensor - O(n) search is acceptable since events are infrequent + for (auto *child : this->children_) { + if (child->get_touch_pad() != event.pad) { + continue; + } + + // Found matching pad - process it + child->value_ = event.value; + + // The interrupt gives us the touch state directly + bool new_state = event.is_touched; + + // Track when we last saw this pad as touched + if (new_state) { + this->last_touch_time_[event.pad] = now; + } + + // Only publish if state changed - this filters out repeated events + if (new_state != child->last_state_) { + child->last_state_ = new_state; + child->publish_state(new_state); + // Original ESP32: ISR only fires when touched, release is detected by timeout + // Note: ESP32 v1 uses inverted logic - touched when value < threshold + ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " < threshold: %" PRIu32 ")", + child->get_name().c_str(), event.value, child->get_threshold()); + } + break; // Exit inner loop after processing matching pad + } + } + + // Check for released pads periodically + if (!this->should_check_for_releases_(now)) { + return; + } + + size_t pads_off = 0; + for (auto *child : this->children_) { + touch_pad_t pad = child->get_touch_pad(); + + // Handle initial state publication after startup + this->publish_initial_state_if_needed_(child, now); + + if (child->last_state_) { + // Pad is currently in touched state - check for release timeout + // Using subtraction handles 32-bit rollover correctly + uint32_t time_diff = now - this->last_touch_time_[pad]; + + // Check if we haven't seen this pad recently + if (time_diff > this->release_timeout_ms_) { + // Haven't seen this pad recently, assume it's released + child->last_state_ = false; + child->publish_state(false); + ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (timeout)", child->get_name().c_str()); + pads_off++; + } + } else { + // Pad is already off + pads_off++; + } + } + + // Disable the loop to save CPU cycles when all pads are off and not in setup mode. + // The loop will be re-enabled by the ISR when any touch pad is touched. + // v1 hardware limitations require us to check all pads are off because: + // - v1 only generates interrupts on touch events (not releases) + // - We must poll for release timeouts in the main loop + // - We can only safely disable when no pads need timeout monitoring + this->check_and_disable_loop_if_all_released_(pads_off); +} + +void ESP32TouchComponent::on_shutdown() { + touch_pad_intr_disable(); + touch_pad_isr_deregister(touch_isr_handler, this); + this->cleanup_touch_queue_(); + + if (this->iir_filter_enabled_()) { + touch_pad_filter_stop(); + touch_pad_filter_delete(); + } + + // Configure wakeup pads if any are set + this->configure_wakeup_pads_(); +} + +void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { + ESP32TouchComponent *component = static_cast(arg); + + touch_pad_clear_status(); + + // INTERRUPT BEHAVIOR: On ESP32 v1 hardware, the interrupt fires when ANY configured + // touch pad detects a touch (value goes below threshold). The hardware does NOT + // generate interrupts on release - only on touch events. + // The interrupt will continue to fire periodically (based on sleep_cycle) as long + // as any pad remains touched. This allows us to detect both new touches and + // continued touches, but releases must be detected by timeout in the main loop. + + // Process all configured pads to check their current state + // Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt, + // so we must scan all configured pads to find which ones were touched + for (auto *child : component->children_) { + touch_pad_t pad = child->get_touch_pad(); + + // Read current value using ISR-safe API + uint32_t value; + if (component->iir_filter_enabled_()) { + uint16_t temp_value = 0; + touch_pad_read_filtered(pad, &temp_value); + value = temp_value; + } else { + // Use low-level HAL function when filter is not enabled + value = touch_ll_read_raw_data(pad); + } + + // Skip pads with 0 value - they haven't been measured in this cycle + // This is important: not all pads are measured every interrupt cycle, + // only those that the hardware has updated + if (value == 0) { + continue; + } + + // IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2! + // ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE + // Therefore: touched = (value < threshold) + // This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold) + bool is_touched = value < child->get_threshold(); + + // Always send the current state - the main loop will filter for changes + // We send both touched and untouched states because the ISR doesn't + // track previous state (to keep ISR fast and simple) + TouchPadEventV1 event; + event.pad = pad; + event.value = value; + event.is_touched = is_touched; + + // Send to queue from ISR - non-blocking, drops if queue full + BaseType_t x_higher_priority_task_woken = pdFALSE; + xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken); + component->enable_loop_soon_any_context(); + if (x_higher_priority_task_woken) { + portYIELD_FROM_ISR(); + } + } +} + +} // namespace esp32_touch +} // namespace esphome + +#endif // USE_ESP32_VARIANT_ESP32 diff --git a/esphome/components/esp32_touch/esp32_touch_v2.cpp b/esphome/components/esp32_touch/esp32_touch_v2.cpp new file mode 100644 index 0000000000..ad77881724 --- /dev/null +++ b/esphome/components/esp32_touch/esp32_touch_v2.cpp @@ -0,0 +1,398 @@ +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + +#include "esp32_touch.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace esp32_touch { + +static const char *const TAG = "esp32_touch"; + +// Helper to update touch state with a known state +void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched) { + // Always update timer when touched + if (is_touched) { + this->last_touch_time_[child->get_touch_pad()] = App.get_loop_component_start_time(); + } + + if (child->last_state_ != is_touched) { + // Read value for logging + uint32_t value = this->read_touch_value(child->get_touch_pad()); + + child->last_state_ = is_touched; + child->publish_state(is_touched); + if (is_touched) { + // ESP32-S2/S3 v2: touched when value > threshold + ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " > threshold: %" PRIu32 ")", child->get_name().c_str(), + value, child->get_threshold()); + } else { + ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str()); + } + } +} + +// Helper to read touch value and update state for a given child (used for timeout events) +bool ESP32TouchComponent::check_and_update_touch_state_(ESP32TouchBinarySensor *child) { + // Read current touch value + uint32_t value = this->read_touch_value(child->get_touch_pad()); + + // ESP32-S2/S3 v2: Touch is detected when value > threshold + bool is_touched = value > child->get_threshold(); + + this->update_touch_state_(child, is_touched); + return is_touched; +} + +void ESP32TouchComponent::setup() { + // Create queue for touch events first + if (!this->create_touch_queue_()) { + return; + } + + // Initialize touch pad peripheral + esp_err_t init_err = touch_pad_init(); + if (init_err != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize touch pad: %s", esp_err_to_name(init_err)); + this->mark_failed(); + return; + } + + // Configure each touch pad first + for (auto *child : this->children_) { + esp_err_t config_err = touch_pad_config(child->get_touch_pad()); + if (config_err != ESP_OK) { + ESP_LOGE(TAG, "Failed to configure touch pad %d: %s", child->get_touch_pad(), esp_err_to_name(config_err)); + } + } + + // Set up filtering if configured + if (this->filter_configured_()) { + touch_filter_config_t filter_info = { + .mode = this->filter_mode_, + .debounce_cnt = this->debounce_count_, + .noise_thr = this->noise_threshold_, + .jitter_step = this->jitter_step_, + .smh_lvl = this->smooth_level_, + }; + touch_pad_filter_set_config(&filter_info); + touch_pad_filter_enable(); + } + + if (this->denoise_configured_()) { + touch_pad_denoise_t denoise = { + .grade = this->grade_, + .cap_level = this->cap_level_, + }; + touch_pad_denoise_set_config(&denoise); + touch_pad_denoise_enable(); + } + + if (this->waterproof_configured_()) { + touch_pad_waterproof_t waterproof = { + .guard_ring_pad = this->waterproof_guard_ring_pad_, + .shield_driver = this->waterproof_shield_driver_, + }; + touch_pad_waterproof_set_config(&waterproof); + touch_pad_waterproof_enable(); + } + + // Configure measurement parameters + touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_); + // ESP32-S2/S3 always use the older API + touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_); + + // Configure timeout if needed + touch_pad_timeout_set(true, TOUCH_PAD_THRESHOLD_MAX); + + // Register ISR handler with interrupt mask + esp_err_t err = + touch_pad_isr_register(touch_isr_handler, this, static_cast(TOUCH_PAD_INTR_MASK_ALL)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to register touch ISR: %s", esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + + // Set thresholds for each pad BEFORE starting FSM + for (auto *child : this->children_) { + if (child->get_threshold() != 0) { + touch_pad_set_thresh(child->get_touch_pad(), child->get_threshold()); + } + } + + // Enable interrupts - only ACTIVE and TIMEOUT + // NOTE: We intentionally don't enable INACTIVE interrupts because they are unreliable + // on ESP32-S2/S3 hardware and sometimes don't fire. Instead, we use timeout-based + // release detection with the ability to verify the actual state. + touch_pad_intr_enable(static_cast(TOUCH_PAD_INTR_MASK_ACTIVE | TOUCH_PAD_INTR_MASK_TIMEOUT)); + + // Set FSM mode before starting + touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); + + // Start FSM + touch_pad_fsm_start(); + + // Calculate release timeout based on sleep cycle + this->calculate_release_timeout_(); +} + +void ESP32TouchComponent::dump_config() { + this->dump_config_base_(); + + if (this->filter_configured_()) { + const char *filter_mode_s; + switch (this->filter_mode_) { + case TOUCH_PAD_FILTER_IIR_4: + filter_mode_s = "IIR_4"; + break; + case TOUCH_PAD_FILTER_IIR_8: + filter_mode_s = "IIR_8"; + break; + case TOUCH_PAD_FILTER_IIR_16: + filter_mode_s = "IIR_16"; + break; + case TOUCH_PAD_FILTER_IIR_32: + filter_mode_s = "IIR_32"; + break; + case TOUCH_PAD_FILTER_IIR_64: + filter_mode_s = "IIR_64"; + break; + case TOUCH_PAD_FILTER_IIR_128: + filter_mode_s = "IIR_128"; + break; + case TOUCH_PAD_FILTER_IIR_256: + filter_mode_s = "IIR_256"; + break; + case TOUCH_PAD_FILTER_JITTER: + filter_mode_s = "JITTER"; + break; + default: + filter_mode_s = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, + " Filter mode: %s\n" + " Debounce count: %" PRIu32 "\n" + " Noise threshold coefficient: %" PRIu32 "\n" + " Jitter filter step size: %" PRIu32, + filter_mode_s, this->debounce_count_, this->noise_threshold_, this->jitter_step_); + const char *smooth_level_s; + switch (this->smooth_level_) { + case TOUCH_PAD_SMOOTH_OFF: + smooth_level_s = "OFF"; + break; + case TOUCH_PAD_SMOOTH_IIR_2: + smooth_level_s = "IIR_2"; + break; + case TOUCH_PAD_SMOOTH_IIR_4: + smooth_level_s = "IIR_4"; + break; + case TOUCH_PAD_SMOOTH_IIR_8: + smooth_level_s = "IIR_8"; + break; + default: + smooth_level_s = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " Smooth level: %s", smooth_level_s); + } + + if (this->denoise_configured_()) { + const char *grade_s; + switch (this->grade_) { + case TOUCH_PAD_DENOISE_BIT12: + grade_s = "BIT12"; + break; + case TOUCH_PAD_DENOISE_BIT10: + grade_s = "BIT10"; + break; + case TOUCH_PAD_DENOISE_BIT8: + grade_s = "BIT8"; + break; + case TOUCH_PAD_DENOISE_BIT4: + grade_s = "BIT4"; + break; + default: + grade_s = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " Denoise grade: %s", grade_s); + + const char *cap_level_s; + switch (this->cap_level_) { + case TOUCH_PAD_DENOISE_CAP_L0: + cap_level_s = "L0"; + break; + case TOUCH_PAD_DENOISE_CAP_L1: + cap_level_s = "L1"; + break; + case TOUCH_PAD_DENOISE_CAP_L2: + cap_level_s = "L2"; + break; + case TOUCH_PAD_DENOISE_CAP_L3: + cap_level_s = "L3"; + break; + case TOUCH_PAD_DENOISE_CAP_L4: + cap_level_s = "L4"; + break; + case TOUCH_PAD_DENOISE_CAP_L5: + cap_level_s = "L5"; + break; + case TOUCH_PAD_DENOISE_CAP_L6: + cap_level_s = "L6"; + break; + case TOUCH_PAD_DENOISE_CAP_L7: + cap_level_s = "L7"; + break; + default: + cap_level_s = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " Denoise capacitance level: %s", cap_level_s); + } + + if (this->setup_mode_) { + ESP_LOGCONFIG(TAG, " Setup Mode ENABLED"); + } + + this->dump_config_sensors_(); +} + +void ESP32TouchComponent::loop() { + const uint32_t now = App.get_loop_component_start_time(); + + // V2 TOUCH HANDLING: + // Due to unreliable INACTIVE interrupts on ESP32-S2/S3, we use a hybrid approach: + // 1. Process ACTIVE interrupts when pads are touched + // 2. Use timeout-based release detection (like v1) + // 3. But smarter than v1: verify actual state before releasing on timeout + // This prevents false releases if we missed interrupts + + // In setup mode, periodically log all pad values + this->process_setup_mode_logging_(now); + + // Process any queued touch events from interrupts + TouchPadEventV2 event; + while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) { + // Handle timeout events + if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) { + // Resume measurement after timeout + touch_pad_timeout_resume(); + // For timeout events, always check the current state + } else if (!(event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE)) { + // Skip if not an active/timeout event + continue; + } + + // Find the child for the pad that triggered the interrupt + for (auto *child : this->children_) { + if (child->get_touch_pad() != event.pad) { + continue; + } + + if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) { + // For timeout events, we need to read the value to determine state + this->check_and_update_touch_state_(child); + } else if (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) { + // We only get ACTIVE interrupts now, releases are detected by timeout + this->update_touch_state_(child, true); // Always touched for ACTIVE interrupts + } + break; + } + } + + // Check for released pads periodically (like v1) + if (!this->should_check_for_releases_(now)) { + return; + } + + size_t pads_off = 0; + for (auto *child : this->children_) { + touch_pad_t pad = child->get_touch_pad(); + + // Handle initial state publication after startup + this->publish_initial_state_if_needed_(child, now); + + if (child->last_state_) { + // Pad is currently in touched state - check for release timeout + // Using subtraction handles 32-bit rollover correctly + uint32_t time_diff = now - this->last_touch_time_[pad]; + + // Check if we haven't seen this pad recently + if (time_diff > this->release_timeout_ms_) { + // Haven't seen this pad recently - verify actual state + // Unlike v1, v2 hardware allows us to read the current state anytime + // This makes v2 smarter: we can verify if it's actually released before + // declaring a timeout, preventing false releases if interrupts were missed + bool still_touched = this->check_and_update_touch_state_(child); + + if (still_touched) { + // Still touched! Timer was reset in update_touch_state_ + ESP_LOGVV(TAG, "Touch Pad '%s' still touched after %" PRIu32 "ms timeout, resetting timer", + child->get_name().c_str(), this->release_timeout_ms_); + } else { + // Actually released - already handled by check_and_update_touch_state_ + pads_off++; + } + } + } else { + // Pad is already off + pads_off++; + } + } + + // Disable the loop when all pads are off and not in setup mode (like v1) + // We need to keep checking for timeouts, so only disable when all pads are confirmed off + this->check_and_disable_loop_if_all_released_(pads_off); +} + +void ESP32TouchComponent::on_shutdown() { + // Disable interrupts + touch_pad_intr_disable(static_cast(TOUCH_PAD_INTR_MASK_ACTIVE | TOUCH_PAD_INTR_MASK_TIMEOUT)); + touch_pad_isr_deregister(touch_isr_handler, this); + this->cleanup_touch_queue_(); + + // Configure wakeup pads if any are set + this->configure_wakeup_pads_(); +} + +void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { + ESP32TouchComponent *component = static_cast(arg); + BaseType_t x_higher_priority_task_woken = pdFALSE; + + // Read interrupt status + TouchPadEventV2 event; + event.intr_mask = touch_pad_read_intr_status_mask(); + event.pad = touch_pad_get_current_meas_channel(); + + // Send event to queue for processing in main loop + xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken); + component->enable_loop_soon_any_context(); + + if (x_higher_priority_task_woken) { + portYIELD_FROM_ISR(); + } +} + +uint32_t ESP32TouchComponent::read_touch_value(touch_pad_t pad) const { + // Unlike ESP32 v1, touch reads on ESP32-S2/S3 v2 are non-blocking operations. + // The hardware continuously samples in the background and we can read the + // latest value at any time without waiting. + uint32_t value = 0; + if (this->filter_configured_()) { + // Read filtered/smoothed value when filter is enabled + touch_pad_filter_read_smooth(pad, &value); + } else { + // Read raw value when filter is not configured + touch_pad_read_raw_data(pad, &value); + } + return value; +} + +} // namespace esp32_touch +} // namespace esphome + +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 From 16ef5a93774476bcbbb53bc3bcb7fc2ffef61454 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 Jun 2025 22:21:11 -0500 Subject: [PATCH 195/293] Add OTA support to ESP-IDF webserver (#9264) --- .../captive_portal/captive_portal.cpp | 2 + esphome/components/web_server/__init__.py | 22 +- esphome/components/web_server/web_server.cpp | 6 + .../web_server_base/web_server_base.cpp | 210 +++++++++++++-- .../web_server_base/web_server_base.h | 16 ++ esphome/components/web_server_idf/__init__.py | 8 +- .../components/web_server_idf/multipart.cpp | 254 ++++++++++++++++++ esphome/components/web_server_idf/multipart.h | 86 ++++++ esphome/components/web_server_idf/utils.cpp | 32 +++ esphome/components/web_server_idf/utils.h | 10 + .../web_server_idf/web_server_idf.cpp | 128 ++++++++- .../web_server_idf/web_server_idf.h | 3 + esphome/core/defines.h | 1 + esphome/idf_component.yml | 2 + .../web_server/test_no_ota.esp32-idf.yaml | 9 + .../web_server/test_ota.esp32-idf.yaml | 32 +++ .../test_ota_disabled.esp32-idf.yaml | 11 + 17 files changed, 788 insertions(+), 44 deletions(-) create mode 100644 esphome/components/web_server_idf/multipart.cpp create mode 100644 esphome/components/web_server_idf/multipart.h create mode 100644 tests/components/web_server/test_no_ota.esp32-idf.yaml create mode 100644 tests/components/web_server/test_ota.esp32-idf.yaml create mode 100644 tests/components/web_server/test_ota_disabled.esp32-idf.yaml diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 51e5cfc8ff..ba392bb0f2 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -47,7 +47,9 @@ void CaptivePortal::start() { this->base_->init(); if (!this->initialized_) { this->base_->add_handler(this); +#ifdef USE_WEBSERVER_OTA this->base_->add_ota_handler(); +#endif } #ifdef USE_ARDUINO diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index f2c1824028..ca145c732b 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -40,6 +40,7 @@ CONF_SORTING_GROUP_ID = "sorting_group_id" CONF_SORTING_GROUPS = "sorting_groups" CONF_SORTING_WEIGHT = "sorting_weight" + web_server_ns = cg.esphome_ns.namespace("web_server") WebServer = web_server_ns.class_("WebServer", cg.Component, cg.Controller) @@ -72,12 +73,6 @@ def validate_local(config): return config -def validate_ota(config): - if CORE.using_esp_idf and config[CONF_OTA]: - raise cv.Invalid("Enabling 'ota' is not supported for IDF framework yet") - return config - - def validate_sorting_groups(config): if CONF_SORTING_GROUPS in config and config[CONF_VERSION] != 3: raise cv.Invalid( @@ -175,15 +170,7 @@ CONFIG_SCHEMA = cv.All( web_server_base.WebServerBase ), cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean, - cv.SplitDefault( - CONF_OTA, - esp8266=True, - esp32_arduino=True, - esp32_idf=False, - bk72xx=True, - ln882x=True, - rtl87xx=True, - ): cv.boolean, + cv.Optional(CONF_OTA, default=True): cv.boolean, cv.Optional(CONF_LOG, default=True): cv.boolean, cv.Optional(CONF_LOCAL): cv.boolean, cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group), @@ -200,7 +187,6 @@ CONFIG_SCHEMA = cv.All( ), default_url, validate_local, - validate_ota, validate_sorting_groups, ) @@ -286,6 +272,10 @@ async def to_code(config): cg.add(var.set_css_url(config[CONF_CSS_URL])) cg.add(var.set_js_url(config[CONF_JS_URL])) cg.add(var.set_allow_ota(config[CONF_OTA])) + if config[CONF_OTA]: + # Define USE_WEBSERVER_OTA based only on web_server OTA config + # This allows web server OTA to work without loading the OTA component + cg.add_define("USE_WEBSERVER_OTA") cg.add(var.set_expose_log(config[CONF_LOG])) if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]: cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS") diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 669bfbf279..e0027d0b27 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -299,8 +299,10 @@ void WebServer::setup() { #endif this->base_->add_handler(this); +#ifdef USE_WEBSERVER_OTA if (this->allow_ota_) this->base_->add_ota_handler(); +#endif // doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly // getting a lot of events @@ -2030,6 +2032,10 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } #endif + + // No matching handler found - send 404 + ESP_LOGV(TAG, "Request for unknown URL: %s", request->url().c_str()); + request->send(404, "text/plain", "Not Found"); } bool WebServer::isRequestHandlerTrivial() const { return false; } diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp index 2835585387..9ad88e09f4 100644 --- a/esphome/components/web_server_base/web_server_base.cpp +++ b/esphome/components/web_server_base/web_server_base.cpp @@ -14,11 +14,114 @@ #endif #endif +#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) +#include +#include +#endif + namespace esphome { namespace web_server_base { static const char *const TAG = "web_server_base"; +#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) +// Minimal OTA backend implementation for web server +// This allows OTA updates via web server without requiring the OTA component +// TODO: In the future, this should be refactored into a common ota_base component +// that both web_server and ota components can depend on, avoiding code duplication +// while keeping the components independent. This would allow both ESP-IDF and Arduino +// implementations to share the base OTA functionality without requiring the full OTA component. +// The IDFWebServerOTABackend class is intentionally designed with the same interface +// as OTABackend to make it easy to swap to using OTABackend when the ota component +// is split into ota and ota_base in the future. +class IDFWebServerOTABackend { + public: + bool begin() { + this->partition_ = esp_ota_get_next_update_partition(nullptr); + if (this->partition_ == nullptr) { + ESP_LOGE(TAG, "No OTA partition available"); + return false; + } + +#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15 + // The following function takes longer than the default timeout of WDT due to flash erase +#if ESP_IDF_VERSION_MAJOR >= 5 + esp_task_wdt_config_t wdtc; + wdtc.idle_core_mask = 0; +#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0 + wdtc.idle_core_mask |= (1 << 0); +#endif +#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1 + wdtc.idle_core_mask |= (1 << 1); +#endif + wdtc.timeout_ms = 15000; + wdtc.trigger_panic = false; + esp_task_wdt_reconfigure(&wdtc); +#else + esp_task_wdt_init(15, false); +#endif +#endif + + esp_err_t err = esp_ota_begin(this->partition_, 0, &this->update_handle_); + +#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15 + // Set the WDT back to the configured timeout +#if ESP_IDF_VERSION_MAJOR >= 5 + wdtc.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000; + esp_task_wdt_reconfigure(&wdtc); +#else + esp_task_wdt_init(CONFIG_ESP_TASK_WDT_TIMEOUT_S, false); +#endif +#endif + + if (err != ESP_OK) { + esp_ota_abort(this->update_handle_); + this->update_handle_ = 0; + ESP_LOGE(TAG, "esp_ota_begin failed: %s", esp_err_to_name(err)); + return false; + } + return true; + } + + bool write(uint8_t *data, size_t len) { + esp_err_t err = esp_ota_write(this->update_handle_, data, len); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_write failed: %s", esp_err_to_name(err)); + return false; + } + return true; + } + + bool end() { + esp_err_t err = esp_ota_end(this->update_handle_); + this->update_handle_ = 0; + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_end failed: %s", esp_err_to_name(err)); + return false; + } + + err = esp_ota_set_boot_partition(this->partition_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_set_boot_partition failed: %s", esp_err_to_name(err)); + return false; + } + + return true; + } + + void abort() { + if (this->update_handle_ != 0) { + esp_ota_abort(this->update_handle_); + this->update_handle_ = 0; + } + } + + private: + esp_ota_handle_t update_handle_{0}; + const esp_partition_t *partition_{nullptr}; +}; +#endif + void WebServerBase::add_handler(AsyncWebHandler *handler) { // remove all handlers @@ -31,6 +134,33 @@ void WebServerBase::add_handler(AsyncWebHandler *handler) { } } +#ifdef USE_WEBSERVER_OTA +void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) { + const uint32_t now = millis(); + if (now - this->last_ota_progress_ > 1000) { + if (request->contentLength() != 0) { + float percentage = (this->ota_read_length_ * 100.0f) / request->contentLength(); + ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage); + } else { + ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_); + } + this->last_ota_progress_ = now; + } +} + +void OTARequestHandler::schedule_ota_reboot_() { + ESP_LOGI(TAG, "OTA update successful!"); + this->parent_->set_timeout(100, []() { + ESP_LOGI(TAG, "Performing OTA reboot now"); + App.safe_reboot(); + }); +} + +void OTARequestHandler::ota_init_(const char *filename) { + ESP_LOGI(TAG, "OTA Update Start: %s", filename); + this->ota_read_length_ = 0; +} + void report_ota_error() { #ifdef USE_ARDUINO StreamString ss; @@ -44,8 +174,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin #ifdef USE_ARDUINO bool success; if (index == 0) { - ESP_LOGI(TAG, "OTA Update Start: %s", filename.c_str()); - this->ota_read_length_ = 0; + this->ota_init_(filename.c_str()); #ifdef USE_ESP8266 Update.runAsync(true); // NOLINTNEXTLINE(readability-static-accessed-through-instance) @@ -72,31 +201,68 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin return; } this->ota_read_length_ += len; - - const uint32_t now = millis(); - if (now - this->last_ota_progress_ > 1000) { - if (request->contentLength() != 0) { - float percentage = (this->ota_read_length_ * 100.0f) / request->contentLength(); - ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage); - } else { - ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_); - } - this->last_ota_progress_ = now; - } + this->report_ota_progress_(request); if (final) { if (Update.end(true)) { - ESP_LOGI(TAG, "OTA update successful!"); - this->parent_->set_timeout(100, []() { App.safe_reboot(); }); + this->schedule_ota_reboot_(); } else { report_ota_error(); } } -#endif +#endif // USE_ARDUINO + +#ifdef USE_ESP_IDF + // ESP-IDF implementation + if (index == 0 && !this->ota_backend_) { + // Initialize OTA on first call + this->ota_init_(filename.c_str()); + this->ota_success_ = false; + + auto *backend = new IDFWebServerOTABackend(); + if (!backend->begin()) { + ESP_LOGE(TAG, "OTA begin failed"); + delete backend; + return; + } + this->ota_backend_ = backend; + } + + auto *backend = static_cast(this->ota_backend_); + if (!backend) { + return; + } + + // Process data + if (len > 0) { + if (!backend->write(data, len)) { + ESP_LOGE(TAG, "OTA write failed"); + backend->abort(); + delete backend; + this->ota_backend_ = nullptr; + return; + } + this->ota_read_length_ += len; + this->report_ota_progress_(request); + } + + // Finalize + if (final) { + this->ota_success_ = backend->end(); + if (this->ota_success_) { + this->schedule_ota_reboot_(); + } else { + ESP_LOGE(TAG, "OTA end failed"); + } + delete backend; + this->ota_backend_ = nullptr; + } +#endif // USE_ESP_IDF } + void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) { -#ifdef USE_ARDUINO AsyncWebServerResponse *response; +#ifdef USE_ARDUINO if (!Update.hasError()) { response = request->beginResponse(200, "text/plain", "Update Successful!"); } else { @@ -105,16 +271,20 @@ void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) { Update.printError(ss); response = request->beginResponse(200, "text/plain", ss); } +#endif // USE_ARDUINO +#ifdef USE_ESP_IDF + // Send response based on the OTA result + response = request->beginResponse(200, "text/plain", this->ota_success_ ? "Update Successful!" : "Update Failed!"); +#endif // USE_ESP_IDF response->addHeader("Connection", "close"); request->send(response); -#endif } void WebServerBase::add_ota_handler() { -#ifdef USE_ARDUINO this->add_handler(new OTARequestHandler(this)); // NOLINT -#endif } +#endif + float WebServerBase::get_setup_priority() const { // Before WiFi (captive portal) return setup_priority::WIFI + 2.0f; diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index 641006cb99..09a41956c9 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -110,13 +110,17 @@ class WebServerBase : public Component { void add_handler(AsyncWebHandler *handler); +#ifdef USE_WEBSERVER_OTA void add_ota_handler(); +#endif void set_port(uint16_t port) { port_ = port; } uint16_t get_port() const { return port_; } protected: +#ifdef USE_WEBSERVER_OTA friend class OTARequestHandler; +#endif int initialized_{0}; uint16_t port_{80}; @@ -125,6 +129,7 @@ class WebServerBase : public Component { internal::Credentials credentials_; }; +#ifdef USE_WEBSERVER_OTA class OTARequestHandler : public AsyncWebHandler { public: OTARequestHandler(WebServerBase *parent) : parent_(parent) {} @@ -139,10 +144,21 @@ class OTARequestHandler : public AsyncWebHandler { bool isRequestHandlerTrivial() const override { return false; } protected: + void report_ota_progress_(AsyncWebServerRequest *request); + void schedule_ota_reboot_(); + void ota_init_(const char *filename); + uint32_t last_ota_progress_{0}; uint32_t ota_read_length_{0}; WebServerBase *parent_; + + private: +#ifdef USE_ESP_IDF + void *ota_backend_{nullptr}; + bool ota_success_{false}; +#endif }; +#endif // USE_WEBSERVER_OTA } // namespace web_server_base } // namespace esphome diff --git a/esphome/components/web_server_idf/__init__.py b/esphome/components/web_server_idf/__init__.py index 506e1c5c13..fe1c6f2640 100644 --- a/esphome/components/web_server_idf/__init__.py +++ b/esphome/components/web_server_idf/__init__.py @@ -1,5 +1,7 @@ -from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option import esphome.config_validation as cv +from esphome.const import CONF_OTA, CONF_WEB_SERVER +from esphome.core import CORE CODEOWNERS = ["@dentra"] @@ -12,3 +14,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): # Increase the maximum supported size of headers section in HTTP request packet to be processed by the server add_idf_sdkconfig_option("CONFIG_HTTPD_MAX_REQ_HDR_LEN", 1024) + # Check if web_server component has OTA enabled + if CORE.config.get(CONF_WEB_SERVER, {}).get(CONF_OTA, True): + # Add multipart parser component for ESP-IDF OTA support + add_idf_component(name="zorxx/multipart-parser", ref="1.0.1") diff --git a/esphome/components/web_server_idf/multipart.cpp b/esphome/components/web_server_idf/multipart.cpp new file mode 100644 index 0000000000..8655226ab9 --- /dev/null +++ b/esphome/components/web_server_idf/multipart.cpp @@ -0,0 +1,254 @@ +#include "esphome/core/defines.h" +#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) +#include "multipart.h" +#include "utils.h" +#include "esphome/core/log.h" +#include +#include "multipart_parser.h" + +namespace esphome { +namespace web_server_idf { + +static const char *const TAG = "multipart"; + +// ========== MultipartReader Implementation ========== + +MultipartReader::MultipartReader(const std::string &boundary) { + // Initialize settings with callbacks + memset(&settings_, 0, sizeof(settings_)); + settings_.on_header_field = on_header_field; + settings_.on_header_value = on_header_value; + settings_.on_part_data = on_part_data; + settings_.on_part_data_end = on_part_data_end; + + ESP_LOGV(TAG, "Initializing multipart parser with boundary: '%s' (len: %zu)", boundary.c_str(), boundary.length()); + + // Create parser with boundary + parser_ = multipart_parser_init(boundary.c_str(), &settings_); + if (parser_) { + multipart_parser_set_data(parser_, this); + } else { + ESP_LOGE(TAG, "Failed to initialize multipart parser"); + } +} + +MultipartReader::~MultipartReader() { + if (parser_) { + multipart_parser_free(parser_); + } +} + +size_t MultipartReader::parse(const char *data, size_t len) { + if (!parser_) { + ESP_LOGE(TAG, "Parser not initialized"); + return 0; + } + + size_t parsed = multipart_parser_execute(parser_, data, len); + + if (parsed != len) { + ESP_LOGW(TAG, "Parser consumed %zu of %zu bytes - possible error", parsed, len); + } + + return parsed; +} + +void MultipartReader::process_header_(const char *value, size_t length) { + // Process the completed header (field + value pair) + std::string value_str(value, length); + + if (str_startswith_case_insensitive(current_header_field_, "content-disposition")) { + // Parse name and filename from Content-Disposition + current_part_.name = extract_header_param(value_str, "name"); + current_part_.filename = extract_header_param(value_str, "filename"); + } else if (str_startswith_case_insensitive(current_header_field_, "content-type")) { + current_part_.content_type = str_trim(value_str); + } + + // Clear field for next header + current_header_field_.clear(); +} + +int MultipartReader::on_header_field(multipart_parser *parser, const char *at, size_t length) { + MultipartReader *reader = static_cast(multipart_parser_get_data(parser)); + reader->current_header_field_.assign(at, length); + return 0; +} + +int MultipartReader::on_header_value(multipart_parser *parser, const char *at, size_t length) { + MultipartReader *reader = static_cast(multipart_parser_get_data(parser)); + reader->process_header_(at, length); + return 0; +} + +int MultipartReader::on_part_data(multipart_parser *parser, const char *at, size_t length) { + MultipartReader *reader = static_cast(multipart_parser_get_data(parser)); + // Only process file uploads + if (reader->has_file() && reader->data_callback_) { + // IMPORTANT: The 'at' pointer points to data within the parser's input buffer. + // This data is only valid during this callback. The callback handler MUST + // process or copy the data immediately - it cannot store the pointer for + // later use as the buffer will be overwritten. + reader->data_callback_(reinterpret_cast(at), length); + } + return 0; +} + +int MultipartReader::on_part_data_end(multipart_parser *parser) { + MultipartReader *reader = static_cast(multipart_parser_get_data(parser)); + ESP_LOGV(TAG, "Part data end"); + if (reader->part_complete_callback_) { + reader->part_complete_callback_(); + } + // Clear part info for next part + reader->current_part_ = Part{}; + return 0; +} + +// ========== Utility Functions ========== + +// Case-insensitive string prefix check +bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix) { + if (str.length() < prefix.length()) { + return false; + } + return str_ncmp_ci(str.c_str(), prefix.c_str(), prefix.length()); +} + +// Extract a parameter value from a header line +// Handles both quoted and unquoted values +std::string extract_header_param(const std::string &header, const std::string ¶m) { + size_t search_pos = 0; + + while (search_pos < header.length()) { + // Look for param name + const char *found = stristr(header.c_str() + search_pos, param.c_str()); + if (!found) { + return ""; + } + size_t pos = found - header.c_str(); + + // Check if this is a word boundary (not part of another parameter) + if (pos > 0 && header[pos - 1] != ' ' && header[pos - 1] != ';' && header[pos - 1] != '\t') { + search_pos = pos + 1; + continue; + } + + // Move past param name + pos += param.length(); + + // Skip whitespace and find '=' + while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) { + pos++; + } + + if (pos >= header.length() || header[pos] != '=') { + search_pos = pos; + continue; + } + + pos++; // Skip '=' + + // Skip whitespace after '=' + while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) { + pos++; + } + + if (pos >= header.length()) { + return ""; + } + + // Check if value is quoted + if (header[pos] == '"') { + pos++; + size_t end = header.find('"', pos); + if (end != std::string::npos) { + return header.substr(pos, end - pos); + } + // Malformed - no closing quote + return ""; + } + + // Unquoted value - find the end (semicolon, comma, or end of string) + size_t end = pos; + while (end < header.length() && header[end] != ';' && header[end] != ',' && header[end] != ' ' && + header[end] != '\t') { + end++; + } + + return header.substr(pos, end - pos); + } + + return ""; +} + +// Parse boundary from Content-Type header +// Returns true if boundary found, false otherwise +// boundary_start and boundary_len will point to the boundary value +bool parse_multipart_boundary(const char *content_type, const char **boundary_start, size_t *boundary_len) { + if (!content_type) { + return false; + } + + // Check for multipart/form-data (case-insensitive) + if (!stristr(content_type, "multipart/form-data")) { + return false; + } + + // Look for boundary parameter + const char *b = stristr(content_type, "boundary="); + if (!b) { + return false; + } + + const char *start = b + 9; // Skip "boundary=" + + // Skip whitespace + while (*start == ' ' || *start == '\t') { + start++; + } + + if (!*start) { + return false; + } + + // Find end of boundary + const char *end = start; + if (*end == '"') { + // Quoted boundary + start++; + end++; + while (*end && *end != '"') { + end++; + } + *boundary_len = end - start; + } else { + // Unquoted boundary + while (*end && *end != ' ' && *end != ';' && *end != '\r' && *end != '\n' && *end != '\t') { + end++; + } + *boundary_len = end - start; + } + + if (*boundary_len == 0) { + return false; + } + + *boundary_start = start; + + return true; +} + +// Trim whitespace from both ends of a string +std::string str_trim(const std::string &str) { + size_t start = str.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) { + return ""; + } + size_t end = str.find_last_not_of(" \t\r\n"); + return str.substr(start, end - start + 1); +} + +} // namespace web_server_idf +} // namespace esphome +#endif // defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) diff --git a/esphome/components/web_server_idf/multipart.h b/esphome/components/web_server_idf/multipart.h new file mode 100644 index 0000000000..967c72ffa5 --- /dev/null +++ b/esphome/components/web_server_idf/multipart.h @@ -0,0 +1,86 @@ +#pragma once +#include "esphome/core/defines.h" +#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) + +#include +#include +#include +#include +#include +#include +#include + +namespace esphome { +namespace web_server_idf { + +// Wrapper around zorxx/multipart-parser for ESP-IDF OTA uploads +class MultipartReader { + public: + struct Part { + std::string name; + std::string filename; + std::string content_type; + }; + + // IMPORTANT: The data pointer in DataCallback is only valid during the callback! + // The multipart parser passes pointers to its internal buffer which will be + // overwritten after the callback returns. Callbacks MUST process or copy the + // data immediately - storing the pointer for deferred processing will result + // in use-after-free bugs. + using DataCallback = std::function; + using PartCompleteCallback = std::function; + + explicit MultipartReader(const std::string &boundary); + ~MultipartReader(); + + // Set callbacks for handling data + void set_data_callback(DataCallback callback) { data_callback_ = std::move(callback); } + void set_part_complete_callback(PartCompleteCallback callback) { part_complete_callback_ = std::move(callback); } + + // Parse incoming data + size_t parse(const char *data, size_t len); + + // Get current part info + const Part &get_current_part() const { return current_part_; } + + // Check if we found a file upload + bool has_file() const { return !current_part_.filename.empty(); } + + private: + static int on_header_field(multipart_parser *parser, const char *at, size_t length); + static int on_header_value(multipart_parser *parser, const char *at, size_t length); + static int on_part_data(multipart_parser *parser, const char *at, size_t length); + static int on_part_data_end(multipart_parser *parser); + + multipart_parser *parser_{nullptr}; + multipart_parser_settings settings_{}; + + Part current_part_; + std::string current_header_field_; + + DataCallback data_callback_; + PartCompleteCallback part_complete_callback_; + + void process_header_(const char *value, size_t length); +}; + +// ========== Utility Functions ========== + +// Case-insensitive string prefix check +bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix); + +// Extract a parameter value from a header line +// Handles both quoted and unquoted values +std::string extract_header_param(const std::string &header, const std::string ¶m); + +// Parse boundary from Content-Type header +// Returns true if boundary found, false otherwise +// boundary_start and boundary_len will point to the boundary value +bool parse_multipart_boundary(const char *content_type, const char **boundary_start, size_t *boundary_len); + +// Trim whitespace from both ends of a string +std::string str_trim(const std::string &str); + +} // namespace web_server_idf +} // namespace esphome +#endif // defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index 349acce50d..ac5df90bb8 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -1,5 +1,7 @@ #ifdef USE_ESP_IDF #include +#include +#include #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "http_parser.h" @@ -88,6 +90,36 @@ optional query_key_value(const std::string &query_url, const std::s return {val.get()}; } +// Helper function for case-insensitive string region comparison +bool str_ncmp_ci(const char *s1, const char *s2, size_t n) { + for (size_t i = 0; i < n; i++) { + if (!char_equals_ci(s1[i], s2[i])) { + return false; + } + } + return true; +} + +// Case-insensitive string search (like strstr but case-insensitive) +const char *stristr(const char *haystack, const char *needle) { + if (!haystack) { + return nullptr; + } + + size_t needle_len = strlen(needle); + if (needle_len == 0) { + return haystack; + } + + for (const char *p = haystack; *p; p++) { + if (str_ncmp_ci(p, needle, needle_len)) { + return p; + } + } + + return nullptr; +} + } // namespace web_server_idf } // namespace esphome #endif // USE_ESP_IDF diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index 9ed17c1d50..988b962d72 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -2,6 +2,7 @@ #ifdef USE_ESP_IDF #include +#include #include "esphome/core/helpers.h" namespace esphome { @@ -12,6 +13,15 @@ optional request_get_header(httpd_req_t *req, const char *name); optional request_get_url_query(httpd_req_t *req); optional query_key_value(const std::string &query_url, const std::string &key); +// Helper function for case-insensitive character comparison +inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); } + +// Helper function for case-insensitive string region comparison +bool str_ncmp_ci(const char *s1, const char *s2, size_t n); + +// Case-insensitive string search (like strstr but case-insensitive) +const char *stristr(const char *haystack, const char *needle); + } // namespace web_server_idf } // namespace esphome #endif // USE_ESP_IDF diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 409230806c..9478e4748c 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -1,16 +1,25 @@ #ifdef USE_ESP_IDF #include +#include +#include +#include #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esp_tls_crypto.h" +#include +#include #include "utils.h" - #include "web_server_idf.h" +#ifdef USE_WEBSERVER_OTA +#include +#include "multipart.h" // For parse_multipart_boundary and other utils +#endif + #ifdef USE_WEBSERVER #include "esphome/components/web_server/web_server.h" #include "esphome/components/web_server/list_entities.h" @@ -72,18 +81,32 @@ void AsyncWebServer::begin() { esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { ESP_LOGVV(TAG, "Enter AsyncWebServer::request_post_handler. uri=%s", r->uri); auto content_type = request_get_header(r, "Content-Type"); - if (content_type.has_value() && *content_type != "application/x-www-form-urlencoded") { - ESP_LOGW(TAG, "Only application/x-www-form-urlencoded supported for POST request"); - // fallback to get handler to support backward compatibility - return AsyncWebServer::request_handler(r); - } if (!request_has_header(r, "Content-Length")) { - ESP_LOGW(TAG, "Content length is requred for post: %s", r->uri); + ESP_LOGW(TAG, "Content length is required for post: %s", r->uri); httpd_resp_send_err(r, HTTPD_411_LENGTH_REQUIRED, nullptr); return ESP_OK; } + if (content_type.has_value()) { + const char *content_type_char = content_type.value().c_str(); + + // Check most common case first + if (stristr(content_type_char, "application/x-www-form-urlencoded") != nullptr) { + // Normal form data - proceed with regular handling +#ifdef USE_WEBSERVER_OTA + } else if (stristr(content_type_char, "multipart/form-data") != nullptr) { + auto *server = static_cast(r->user_ctx); + return server->handle_multipart_upload_(r, content_type_char); +#endif + } else { + ESP_LOGW(TAG, "Unsupported content type for POST: %s", content_type_char); + // fallback to get handler to support backward compatibility + return AsyncWebServer::request_handler(r); + } + } + + // Handle regular form data if (r->content_len > HTTPD_MAX_REQ_HDR_LEN) { ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len); httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); @@ -539,6 +562,97 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e } #endif +#ifdef USE_WEBSERVER_OTA +esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *content_type) { + static constexpr size_t MULTIPART_CHUNK_SIZE = 1460; // Match Arduino AsyncWebServer buffer size + static constexpr size_t YIELD_INTERVAL_BYTES = 16 * 1024; // Yield every 16KB to prevent watchdog + + // Parse boundary and create reader + const char *boundary_start; + size_t boundary_len; + if (!parse_multipart_boundary(content_type, &boundary_start, &boundary_len)) { + ESP_LOGE(TAG, "Failed to parse multipart boundary"); + httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); + return ESP_FAIL; + } + + AsyncWebServerRequest req(r); + AsyncWebHandler *handler = nullptr; + for (auto *h : this->handlers_) { + if (h->canHandle(&req)) { + handler = h; + break; + } + } + + if (!handler) { + ESP_LOGW(TAG, "No handler found for OTA request"); + httpd_resp_send_err(r, HTTPD_404_NOT_FOUND, nullptr); + return ESP_OK; + } + + // Upload state + std::string filename; + size_t index = 0; + // Create reader on heap to reduce stack usage + auto reader = std::make_unique("--" + std::string(boundary_start, boundary_len)); + + // Configure callbacks + reader->set_data_callback([&](const uint8_t *data, size_t len) { + if (!reader->has_file() || !len) + return; + + if (filename.empty()) { + filename = reader->get_current_part().filename; + ESP_LOGV(TAG, "Processing file: '%s'", filename.c_str()); + handler->handleUpload(&req, filename, 0, nullptr, 0, false); // Start + } + + handler->handleUpload(&req, filename, index, const_cast(data), len, false); + index += len; + }); + + reader->set_part_complete_callback([&]() { + if (index > 0) { + handler->handleUpload(&req, filename, index, nullptr, 0, true); // End + filename.clear(); + index = 0; + } + }); + + // Process data + std::unique_ptr buffer(new char[MULTIPART_CHUNK_SIZE]); + size_t bytes_since_yield = 0; + + for (size_t remaining = r->content_len; remaining > 0;) { + int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, MULTIPART_CHUNK_SIZE)); + + if (recv_len <= 0) { + httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST, + nullptr); + return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL; + } + + if (reader->parse(buffer.get(), recv_len) != static_cast(recv_len)) { + ESP_LOGW(TAG, "Multipart parser error"); + httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); + return ESP_FAIL; + } + + remaining -= recv_len; + bytes_since_yield += recv_len; + + if (bytes_since_yield > YIELD_INTERVAL_BYTES) { + vTaskDelay(1); + bytes_since_yield = 0; + } + } + + handler->handleRequest(&req); + return ESP_OK; +} +#endif // USE_WEBSERVER_OTA + } // namespace web_server_idf } // namespace esphome diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 7547117224..8de25c8e96 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -204,6 +204,9 @@ class AsyncWebServer { static esp_err_t request_handler(httpd_req_t *r); static esp_err_t request_post_handler(httpd_req_t *r); esp_err_t request_handler_(AsyncWebServerRequest *request) const; +#ifdef USE_WEBSERVER_OTA + esp_err_t handle_multipart_upload_(httpd_req_t *r, const char *content_type); +#endif std::vector handlers_; std::function on_not_found_{}; }; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index ea3c8bdc17..cfaed6fdb7 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -153,6 +153,7 @@ #define USE_SPI #define USE_VOICE_ASSISTANT #define USE_WEBSERVER +#define USE_WEBSERVER_OTA #define USE_WEBSERVER_PORT 80 // NOLINT #define USE_WEBSERVER_SORTING #define USE_WIFI_11KV_SUPPORT diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 6299909033..c43b622684 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -17,3 +17,5 @@ dependencies: version: 2.0.11 rules: - if: "target in [esp32h2, esp32p4]" + zorxx/multipart-parser: + version: 1.0.1 diff --git a/tests/components/web_server/test_no_ota.esp32-idf.yaml b/tests/components/web_server/test_no_ota.esp32-idf.yaml new file mode 100644 index 0000000000..1f677fb948 --- /dev/null +++ b/tests/components/web_server/test_no_ota.esp32-idf.yaml @@ -0,0 +1,9 @@ +packages: + device_base: !include common.yaml + +# No OTA component defined for this test + +web_server: + port: 8080 + version: 2 + ota: false diff --git a/tests/components/web_server/test_ota.esp32-idf.yaml b/tests/components/web_server/test_ota.esp32-idf.yaml new file mode 100644 index 0000000000..294e7f862e --- /dev/null +++ b/tests/components/web_server/test_ota.esp32-idf.yaml @@ -0,0 +1,32 @@ +# Test configuration for ESP-IDF web server with OTA enabled +esphome: + name: test-web-server-ota-idf + +# Force ESP-IDF framework +esp32: + board: esp32dev + framework: + type: esp-idf + +packages: + device_base: !include common.yaml + +# Enable OTA for multipart upload testing +ota: + - platform: esphome + password: "test_ota_password" + +# Web server with OTA enabled +web_server: + port: 8080 + version: 2 + ota: true + include_internal: true + +# Enable debug logging for OTA +logger: + level: DEBUG + logs: + web_server: VERBOSE + web_server_idf: VERBOSE + diff --git a/tests/components/web_server/test_ota_disabled.esp32-idf.yaml b/tests/components/web_server/test_ota_disabled.esp32-idf.yaml new file mode 100644 index 0000000000..c7c7574e3b --- /dev/null +++ b/tests/components/web_server/test_ota_disabled.esp32-idf.yaml @@ -0,0 +1,11 @@ +packages: + device_base: !include common.yaml + +# OTA is configured but web_server OTA is disabled +ota: + - platform: esphome + +web_server: + port: 8080 + version: 2 + ota: false From 35de36d690188623cbd11062aeb59fdc35a4ee1e Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Tue, 1 Jul 2025 05:39:06 +0200 Subject: [PATCH 196/293] [modbus] Modbus server role: write holding registers (#9156) --- esphome/components/modbus/modbus.cpp | 48 +++++++---- esphome/components/modbus/modbus.h | 1 + .../components/modbus_controller/__init__.py | 13 +++ .../modbus_controller/modbus_controller.cpp | 80 +++++++++++++++++++ .../modbus_controller/modbus_controller.h | 15 ++++ .../components/modbus_controller/common.yaml | 13 ++- 6 files changed, 154 insertions(+), 16 deletions(-) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index c2efa93fae..6350f43ef6 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -90,15 +90,24 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { } else { // data starts at 2 and length is 4 for read registers commands - if (this->role == ModbusRole::SERVER && (function_code == 0x1 || function_code == 0x3 || function_code == 0x4)) { - data_offset = 2; - data_len = 4; - } - - // the response for write command mirrors the requests and data starts at offset 2 instead of 3 for read commands - if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) { - data_offset = 2; - data_len = 4; + if (this->role == ModbusRole::SERVER) { + if (function_code == 0x1 || function_code == 0x3 || function_code == 0x4 || function_code == 0x6) { + data_offset = 2; + data_len = 4; + } else if (function_code == 0x10) { + if (at < 6) { + return true; + } + data_offset = 2; + // starting address (2 bytes) + quantity of registers (2 bytes) + byte count itself (1 byte) + actual byte count + data_len = 2 + 2 + 1 + raw[6]; + } + } else { + // the response for write command mirrors the requests and data starts at offset 2 instead of 3 for read commands + if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) { + data_offset = 2; + data_len = 4; + } } // Error ( msb indicates error ) @@ -132,6 +141,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { bool found = false; for (auto *device : this->devices_) { if (device->address_ == address) { + found = true; // Is it an error response? if ((function_code & 0x80) == 0x80) { ESP_LOGD(TAG, "Modbus error function code: 0x%X exception: %d", function_code, raw[2]); @@ -141,13 +151,21 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { // Ignore modbus exception not related to a pending command ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response"); } - } else if (this->role == ModbusRole::SERVER && (function_code == 0x3 || function_code == 0x4)) { - device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8), - uint16_t(data[3]) | (uint16_t(data[2]) << 8)); - } else { - device->on_modbus_data(data); + continue; } - found = true; + if (this->role == ModbusRole::SERVER) { + if (function_code == 0x3 || function_code == 0x4) { + device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8), + uint16_t(data[3]) | (uint16_t(data[2]) << 8)); + continue; + } + if (function_code == 0x6 || function_code == 0x10) { + device->on_modbus_write_registers(function_code, data); + continue; + } + } + // fallthrough for other function codes + device->on_modbus_data(data); } } waiting_for_response = 0; diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index aebdbccc78..ec35612690 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -59,6 +59,7 @@ class ModbusDevice { virtual void on_modbus_data(const std::vector &data) = 0; virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code) {} virtual void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers){}; + virtual void on_modbus_write_registers(uint8_t function_code, const std::vector &data){}; void send(uint8_t function, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0, const uint8_t *payload = nullptr) { this->parent_->send(this->address_, function, start_address, number_of_entities, payload_len, payload); diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 8079b824b0..5ab82f5e17 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -39,6 +39,7 @@ CODEOWNERS = ["@martgras"] AUTO_LOAD = ["modbus"] CONF_READ_LAMBDA = "read_lambda" +CONF_WRITE_LAMBDA = "write_lambda" CONF_SERVER_REGISTERS = "server_registers" MULTI_CONF = True @@ -148,6 +149,7 @@ ModbusServerRegisterSchema = cv.Schema( cv.Required(CONF_ADDRESS): cv.positive_int, cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), cv.Required(CONF_READ_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, } ) @@ -318,6 +320,17 @@ async def to_code(config): ), ) ) + if CONF_WRITE_LAMBDA in server_register: + cg.add( + server_register_var.set_write_lambda( + cg.TemplateArguments(cpp_type), + await cg.process_lambda( + server_register[CONF_WRITE_LAMBDA], + parameters=[(cg.uint16, "address"), (cpp_type, "x")], + return_type=cg.bool_, + ), + ) + ) cg.add(var.add_server_register(server_register_var)) await register_modbus_device(var, config) for conf in config.get(CONF_ON_COMMAND_SENT, []): diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 81e9ccf0a6..0f3ddf920d 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -152,6 +152,86 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t this->send(function_code, start_address, number_of_registers, response.size(), response.data()); } +void ModbusController::on_modbus_write_registers(uint8_t function_code, const std::vector &data) { + uint16_t number_of_registers; + uint16_t payload_offset; + + if (function_code == 0x10) { + number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8); + if (number_of_registers == 0 || number_of_registers > 0x7B) { + ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers); + send_error(function_code, 3); + return; + } + uint16_t payload_size = data[4]; + if (payload_size != number_of_registers * 2) { + ESP_LOGW(TAG, "Payload size of %d bytes is not 2 times the number of registers (%d). Sending exception response.", + payload_size, number_of_registers); + send_error(function_code, 3); + return; + } + payload_offset = 5; + } else if (function_code == 0x06) { + number_of_registers = 1; + payload_offset = 2; + } else { + ESP_LOGW(TAG, "Invalid function code 0x%X. Sending exception response.", function_code); + send_error(function_code, 1); + return; + } + + uint16_t start_address = uint16_t(data[1]) | (uint16_t(data[0]) << 8); + ESP_LOGD(TAG, + "Received write holding registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: " + "0x%X.", + this->address_, function_code, start_address, number_of_registers); + + auto for_each_register = [this, start_address, number_of_registers, payload_offset]( + const std::function &callback) -> bool { + uint16_t offset = payload_offset; + for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) { + bool ok = false; + for (auto *server_register : this->server_registers_) { + if (server_register->address == current_address) { + ok = callback(server_register, offset); + current_address += server_register->register_count; + offset += server_register->register_count * sizeof(uint16_t); + break; + } + } + + if (!ok) { + return false; + } + } + return true; + }; + + // check all registers are writable before writing to any of them: + if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool { + return server_register->write_lambda != nullptr; + })) { + send_error(function_code, 1); + return; + } + + // Actually write to the registers: + if (!for_each_register([&data](ServerRegister *server_register, uint16_t offset) { + int64_t number = payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF); + return server_register->write_lambda(number); + })) { + send_error(function_code, 4); + return; + } + + std::vector response; + response.reserve(6); + response.push_back(this->address_); + response.push_back(function_code); + response.insert(response.end(), data.begin(), data.begin() + 4); + this->send_raw(response); +} + SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const { auto reg_it = std::find_if( std::begin(this->register_ranges_), std::end(this->register_ranges_), diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 11d27c4025..a86ad1ccb5 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -258,6 +258,7 @@ class SensorItem { class ServerRegister { using ReadLambda = std::function; + using WriteLambda = std::function; public: ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count) { @@ -277,6 +278,17 @@ class ServerRegister { }; } + template + void set_write_lambda(const std::function &&user_write_lambda) { + this->write_lambda = [this, user_write_lambda](int64_t number) { + if constexpr (std::is_same_v) { + float float_value = bit_cast(static_cast(number)); + return user_write_lambda(this->address, float_value); + } + return user_write_lambda(this->address, static_cast(number)); + }; + } + // Formats a raw value into a string representation based on the value type for debugging std::string format_value(int64_t value) const { switch (this->value_type) { @@ -304,6 +316,7 @@ class ServerRegister { SensorValueType value_type{SensorValueType::RAW}; uint8_t register_count{0}; ReadLambda read_lambda; + WriteLambda write_lambda; }; // ModbusController::create_register_ranges_ tries to optimize register range @@ -485,6 +498,8 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { void on_modbus_error(uint8_t function_code, uint8_t exception_code) override; /// called when a modbus request (function code 0x03 or 0x04) was parsed without errors void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final; + /// called when a modbus request (function code 0x06 or 0x10) was parsed without errors + void on_modbus_write_registers(uint8_t function_code, const std::vector &data) final; /// default delegate called by process_modbus_data when a response has retrieved from the incoming queue void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector &data); /// default delegate called by process_modbus_data when a response for a write response has retrieved from the diff --git a/tests/components/modbus_controller/common.yaml b/tests/components/modbus_controller/common.yaml index 7fa9f8dae3..7d342ee353 100644 --- a/tests/components/modbus_controller/common.yaml +++ b/tests/components/modbus_controller/common.yaml @@ -33,7 +33,18 @@ modbus_controller: read_lambda: |- return 42.3; max_cmd_retries: 0 - + - id: modbus_controller3 + address: 0x3 + modbus_id: mod_bus2 + server_registers: + - address: 0x0009 + value_type: S_DWORD + read_lambda: |- + return 31; + write_lambda: |- + printf("address=%d, value=%d", x); + return true; + max_cmd_retries: 0 binary_sensor: - platform: modbus_controller modbus_controller_id: modbus_controller1 From 3470305d9d41837a8412eee0b4f6fdf247e6beed Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 1 Jul 2025 12:22:41 -0400 Subject: [PATCH 197/293] [esp32] Remove IDF 4 support and clean up code (#9145) --- esphome/components/adc/adc_sensor.h | 7 +-- esphome/components/esp32/__init__.py | 28 ++++-------- esphome/components/esp32/core.cpp | 4 -- .../components/esp32_ble_tracker/__init__.py | 10 +---- .../components/ethernet/esp_eth_phy_jl1101.c | 16 ------- .../ethernet/ethernet_component.cpp | 17 ------- esphome/components/i2s_audio/__init__.py | 12 +---- esphome/components/i2s_audio/i2s_audio.cpp | 6 +-- .../improv_serial/improv_serial_component.cpp | 4 -- .../internal_temperature.cpp | 35 ++++----------- .../components/internal_temperature/sensor.py | 26 ----------- esphome/components/logger/logger_esp32.cpp | 2 - esphome/components/mdns/__init__.py | 6 +-- .../components/mqtt/mqtt_backend_esp32.cpp | 45 +------------------ esphome/components/opentherm/opentherm.cpp | 19 +++----- .../components/ota/ota_backend_esp_idf.cpp | 11 ----- esphome/components/psram/psram.cpp | 20 --------- .../display/pvvx_display.cpp | 4 -- .../components/rpi_dpi_rgb/rpi_dpi_rgb.cpp | 4 -- esphome/components/st7701s/st7701s.cpp | 4 -- .../uart/uart_component_esp_idf.cpp | 4 -- esphome/components/watchdog/watchdog.cpp | 4 -- esphome/components/wireguard/__init__.py | 13 +----- 23 files changed, 34 insertions(+), 267 deletions(-) diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 9ffb6cf856..28dfd2262c 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -15,8 +15,7 @@ namespace adc { #ifdef USE_ESP32 // clang-format off -#if (ESP_IDF_VERSION_MAJOR == 4 && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 7)) || \ - (ESP_IDF_VERSION_MAJOR == 5 && \ +#if (ESP_IDF_VERSION_MAJOR == 5 && \ ((ESP_IDF_VERSION_MINOR == 0 && ESP_IDF_VERSION_PATCH >= 5) || \ (ESP_IDF_VERSION_MINOR == 1 && ESP_IDF_VERSION_PATCH >= 3) || \ (ESP_IDF_VERSION_MINOR >= 2)) \ @@ -100,11 +99,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage adc1_channel_t channel1_{ADC1_CHANNEL_MAX}; adc2_channel_t channel2_{ADC2_CHANNEL_MAX}; bool autorange_{false}; -#if ESP_IDF_VERSION_MAJOR >= 5 esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {}; -#else - esp_adc_cal_characteristics_t cal_characteristics_[ADC_ATTEN_MAX] = {}; -#endif // ESP_IDF_VERSION_MAJOR #endif // USE_ESP32 }; diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 32323b7504..b4c7a4e05b 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -411,8 +411,8 @@ def _esp_idf_check_versions(value): version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) source = value.get(CONF_SOURCE, None) - if version < cv.Version(4, 0, 0): - raise cv.Invalid("Only ESP-IDF 4.0+ is supported.") + if version < cv.Version(5, 0, 0): + raise cv.Invalid("Only ESP-IDF 5.0+ is supported.") # flag this for later *before* we set value[CONF_PLATFORM_VERSION] below has_platform_ver = CONF_PLATFORM_VERSION in value @@ -422,20 +422,15 @@ def _esp_idf_check_versions(value): ) if ( - (is_platformio := _platform_is_platformio(value[CONF_PLATFORM_VERSION])) - and version.major >= 5 - and version not in SUPPORTED_PLATFORMIO_ESP_IDF_5X - ): + is_platformio := _platform_is_platformio(value[CONF_PLATFORM_VERSION]) + ) and version not in SUPPORTED_PLATFORMIO_ESP_IDF_5X: raise cv.Invalid( f"ESP-IDF {str(version)} not supported by platformio/espressif32" ) if ( - version.major < 5 - or ( - version in SUPPORTED_PLATFORMIO_ESP_IDF_5X - and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X - ) + version in SUPPORTED_PLATFORMIO_ESP_IDF_5X + and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X ) and not has_platform_ver: raise cv.Invalid( f"ESP-IDF {value[CONF_VERSION]} may be supported by platformio/espressif32; please specify '{CONF_PLATFORM_VERSION}'" @@ -801,14 +796,9 @@ async def to_code(config): if advanced.get(CONF_IGNORE_EFUSE_MAC_CRC): add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True) - if (framework_ver.major, framework_ver.minor) >= (4, 4): - add_idf_sdkconfig_option( - "CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False - ) - else: - add_idf_sdkconfig_option( - "CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE", False - ) + add_idf_sdkconfig_option( + "CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False + ) if advanced.get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): _LOGGER.warning( "Using experimental features in ESP-IDF may result in unexpected failures." diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 562bcba3c2..f3bdfea2a0 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -56,11 +56,7 @@ void arch_init() { void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } -#if ESP_IDF_VERSION_MAJOR >= 5 uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } -#else -uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); } -#endif uint32_t arch_get_cpu_freq_hz() { uint32_t freq = 0; #ifdef USE_ESP_IDF diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 2242d709a4..547cf84ed1 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -29,8 +29,6 @@ from esphome.const import ( CONF_ON_BLE_SERVICE_DATA_ADVERTISE, CONF_SERVICE_UUID, CONF_TRIGGER_ID, - KEY_CORE, - KEY_FRAMEWORK_VERSION, ) from esphome.core import CORE @@ -323,10 +321,7 @@ async def to_code(config): # https://github.com/espressif/esp-idf/issues/2503 # Match arduino CONFIG_BTU_TASK_STACK_SIZE # https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866 - if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(4, 4, 6): - add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192) - else: - add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192) + add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192) add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9) add_idf_sdkconfig_option( "CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS] @@ -335,8 +330,7 @@ async def to_code(config): # max notifications in 5.x, setting CONFIG_BT_ACL_CONNECTIONS # is enough in 4.x # https://github.com/esphome/issues/issues/6808 - if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(5, 0, 0): - add_idf_sdkconfig_option("CONFIG_BT_GATTC_NOTIF_REG_MAX", 9) + add_idf_sdkconfig_option("CONFIG_BT_GATTC_NOTIF_REG_MAX", 9) cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts cg.add_define("USE_ESP32_BLE_CLIENT") diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c index de2a6f4f35..4f31e0a9fd 100644 --- a/esphome/components/ethernet/esp_eth_phy_jl1101.c +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -19,11 +19,7 @@ #include #include "esp_log.h" #include "esp_eth.h" -#if ESP_IDF_VERSION_MAJOR >= 5 #include "esp_eth_phy_802_3.h" -#else -#include "eth_phy_regs_struct.h" -#endif #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/gpio.h" @@ -174,11 +170,7 @@ static esp_err_t jl1101_reset_hw(esp_eth_phy_t *phy) { return ESP_OK; } -#if ESP_IDF_VERSION_MAJOR >= 5 static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy, eth_phy_autoneg_cmd_t cmd, bool *nego_state) { -#else -static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy) { -#endif phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); esp_eth_mediator_t *eth = jl1101->eth; /* in case any link status has changed, let's assume we're in link down status */ @@ -293,11 +285,7 @@ static esp_err_t jl1101_init(esp_eth_phy_t *phy) { esp_eth_mediator_t *eth = jl1101->eth; // Detect PHY address if (jl1101->addr == ESP_ETH_PHY_ADDR_AUTO) { -#if ESP_IDF_VERSION_MAJOR >= 5 PHY_CHECK(esp_eth_phy_802_3_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err); -#else - PHY_CHECK(esp_eth_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err); -#endif } /* Power on Ethernet PHY */ PHY_CHECK(jl1101_pwrctl(phy, true) == ESP_OK, "power control failed", err); @@ -336,11 +324,7 @@ esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config) { jl1101->parent.init = jl1101_init; jl1101->parent.deinit = jl1101_deinit; jl1101->parent.set_mediator = jl1101_set_mediator; -#if ESP_IDF_VERSION_MAJOR >= 5 jl1101->parent.autonego_ctrl = jl1101_negotiate; -#else - jl1101->parent.negotiate = jl1101_negotiate; -#endif jl1101->parent.get_link = jl1101_get_link; jl1101->parent.pwrctl = jl1101_pwrctl; jl1101->parent.get_addr = jl1101_get_addr; diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 19a11c6945..f8c2f3a72e 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -122,25 +122,12 @@ void EthernetComponent::setup() { .post_cb = nullptr, }; -#if ESP_IDF_VERSION_MAJOR >= 5 #if CONFIG_ETH_SPI_ETHERNET_W5500 eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg); #endif #if CONFIG_ETH_SPI_ETHERNET_DM9051 eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg); #endif -#else - spi_device_handle_t spi_handle = nullptr; - err = spi_bus_add_device(host, &devcfg, &spi_handle); - ESPHL_ERROR_CHECK(err, "SPI bus add device error"); - -#if CONFIG_ETH_SPI_ETHERNET_W5500 - eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(spi_handle); -#endif -#if CONFIG_ETH_SPI_ETHERNET_DM9051 - eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(spi_handle); -#endif -#endif // ESP_IDF_VERSION_MAJOR >= 5 #if CONFIG_ETH_SPI_ETHERNET_W5500 w5500_config.int_gpio_num = this->interrupt_pin_; @@ -211,11 +198,7 @@ void EthernetComponent::setup() { } case ETHERNET_TYPE_KSZ8081: case ETHERNET_TYPE_KSZ8081RNA: { -#if ESP_IDF_VERSION_MAJOR >= 5 this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config); -#else - this->phy_ = esp_eth_phy_new_ksz8081(&phy_config); -#endif break; } #endif diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index ef95fd0b41..9a2aa0362f 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -9,14 +9,7 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S3, ) import esphome.config_validation as cv -from esphome.const import ( - CONF_BITS_PER_SAMPLE, - CONF_CHANNEL, - CONF_ID, - CONF_SAMPLE_RATE, - KEY_CORE, - KEY_FRAMEWORK_VERSION, -) +from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE from esphome.core import CORE from esphome.cpp_generator import MockObjClass import esphome.final_validate as fv @@ -250,8 +243,7 @@ def _final_validate(_): def use_legacy(): - framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] - if CORE.using_esp_idf and framework_version >= cv.Version(5, 0, 0): + if CORE.using_esp_idf: if not _use_legacy_driver: return False return True diff --git a/esphome/components/i2s_audio/i2s_audio.cpp b/esphome/components/i2s_audio/i2s_audio.cpp index 7ff21bba57..7f233516e6 100644 --- a/esphome/components/i2s_audio/i2s_audio.cpp +++ b/esphome/components/i2s_audio/i2s_audio.cpp @@ -9,15 +9,11 @@ namespace i2s_audio { static const char *const TAG = "i2s_audio"; -#if ESP_IDF_VERSION_MAJOR >= 5 -static const uint8_t I2S_NUM_MAX = SOC_I2S_NUM; // because IDF 5+ took this away :( -#endif - void I2SAudioComponent::setup() { ESP_LOGCONFIG(TAG, "Running setup"); static i2s_port_t next_port_num = I2S_NUM_0; - if (next_port_num >= I2S_NUM_MAX) { + if (next_port_num >= SOC_I2S_NUM) { ESP_LOGE(TAG, "Too many components"); this->mark_failed(); return; diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index c3a0f2eacc..ae4927828b 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -59,11 +59,7 @@ optional ImprovSerialComponent::read_byte_() { break; #if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC) case logger::UART_SELECTION_USB_CDC: -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) if (esp_usb_console_available_for_read()) { -#else - if (esp_usb_console_read_available()) { -#endif esp_usb_console_read_buf((char *) &data, 1); byte = data; } diff --git a/esphome/components/internal_temperature/internal_temperature.cpp b/esphome/components/internal_temperature/internal_temperature.cpp index f503927d8e..85844647f2 100644 --- a/esphome/components/internal_temperature/internal_temperature.cpp +++ b/esphome/components/internal_temperature/internal_temperature.cpp @@ -10,11 +10,7 @@ uint8_t temprature_sens_read(); #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || \ defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32P4) -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) -#include "driver/temp_sensor.h" -#else #include "driver/temperature_sensor.h" -#endif // ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) #endif // USE_ESP32_VARIANT #endif // USE_ESP32 #ifdef USE_RP2040 @@ -31,12 +27,11 @@ namespace internal_temperature { static const char *const TAG = "internal_temperature"; #ifdef USE_ESP32 -#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) && \ - (defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \ - defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2) || \ - defined(USE_ESP32_VARIANT_ESP32P4)) +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \ + defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2) || \ + defined(USE_ESP32_VARIANT_ESP32P4) static temperature_sensor_handle_t tsensNew = NULL; -#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) && USE_ESP32_VARIANT +#endif // USE_ESP32_VARIANT #endif // USE_ESP32 void InternalTemperatureSensor::update() { @@ -51,24 +46,11 @@ void InternalTemperatureSensor::update() { #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || \ defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32P4) -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) - temp_sensor_config_t tsens = TSENS_CONFIG_DEFAULT(); - temp_sensor_set_config(tsens); - temp_sensor_start(); -#if defined(USE_ESP32_VARIANT_ESP32S3) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 3)) -#error \ - "ESP32-S3 internal temperature sensor requires ESP IDF V4.4.3 or higher. See https://github.com/esphome/issues/issues/4271" -#endif - esp_err_t result = temp_sensor_read_celsius(&temperature); - temp_sensor_stop(); - success = (result == ESP_OK); -#else esp_err_t result = temperature_sensor_get_celsius(tsensNew, &temperature); success = (result == ESP_OK); if (!success) { ESP_LOGE(TAG, "Reading failed (%d)", result); } -#endif // ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) #endif // USE_ESP32_VARIANT #endif // USE_ESP32 #ifdef USE_RP2040 @@ -99,10 +81,9 @@ void InternalTemperatureSensor::update() { void InternalTemperatureSensor::setup() { #ifdef USE_ESP32 -#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) && \ - (defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \ - defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2) || \ - defined(USE_ESP32_VARIANT_ESP32P4)) +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \ + defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2) || \ + defined(USE_ESP32_VARIANT_ESP32P4) ESP_LOGCONFIG(TAG, "Running setup"); temperature_sensor_config_t tsens_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80); @@ -120,7 +101,7 @@ void InternalTemperatureSensor::setup() { this->mark_failed(); return; } -#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) && USE_ESP32_VARIANT +#endif // USE_ESP32_VARIANT #endif // USE_ESP32 } diff --git a/esphome/components/internal_temperature/sensor.py b/esphome/components/internal_temperature/sensor.py index 9bfa3739c8..93b98a30f4 100644 --- a/esphome/components/internal_temperature/sensor.py +++ b/esphome/components/internal_temperature/sensor.py @@ -1,46 +1,21 @@ import esphome.codegen as cg from esphome.components import sensor -from esphome.components.esp32 import get_esp32_variant -from esphome.components.esp32.const import VARIANT_ESP32S3 import esphome.config_validation as cv from esphome.const import ( DEVICE_CLASS_TEMPERATURE, ENTITY_CATEGORY_DIAGNOSTIC, - KEY_CORE, - KEY_FRAMEWORK_VERSION, PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_RP2040, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) -from esphome.core import CORE internal_temperature_ns = cg.esphome_ns.namespace("internal_temperature") InternalTemperatureSensor = internal_temperature_ns.class_( "InternalTemperatureSensor", sensor.Sensor, cg.PollingComponent ) - -def validate_config(config): - if CORE.is_esp32: - variant = get_esp32_variant() - if variant == VARIANT_ESP32S3: - if CORE.using_arduino and CORE.data[KEY_CORE][ - KEY_FRAMEWORK_VERSION - ] < cv.Version(2, 0, 6): - raise cv.Invalid( - "ESP32-S3 Internal Temperature Sensor requires framework version 2.0.6 or higher. See ." - ) - if CORE.using_esp_idf and CORE.data[KEY_CORE][ - KEY_FRAMEWORK_VERSION - ] < cv.Version(4, 4, 3): - raise cv.Invalid( - "ESP32-S3 Internal Temperature Sensor requires framework version 4.4.3 or higher. See ." - ) - return config - - CONFIG_SCHEMA = cv.All( sensor.sensor_schema( InternalTemperatureSensor, @@ -51,7 +26,6 @@ CONFIG_SCHEMA = cv.All( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ).extend(cv.polling_component_schema("60s")), cv.only_on([PLATFORM_ESP32, PLATFORM_RP2040, PLATFORM_BK72XX]), - validate_config, ) diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index b5ac84a665..41445fa3b4 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -83,9 +83,7 @@ void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) { uart_config.parity = UART_PARITY_DISABLE; uart_config.stop_bits = UART_STOP_BITS_1; uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) uart_config.source_clk = UART_SCLK_DEFAULT; -#endif uart_param_config(uart_num, &uart_config); const int uart_buffer_size = tx_buffer_size; // Install UART driver using an event queue here diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 2f81068e8a..ed230d43aa 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -8,8 +8,6 @@ from esphome.const import ( CONF_PROTOCOL, CONF_SERVICE, CONF_SERVICES, - KEY_CORE, - KEY_FRAMEWORK_VERSION, ) from esphome.core import CORE, coroutine_with_priority @@ -85,9 +83,7 @@ async def to_code(config): elif CORE.is_rp2040: cg.add_library("LEAmDNS", None) - if CORE.using_esp_idf and CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version( - 5, 0, 0 - ): + if CORE.using_esp_idf: add_idf_component(name="espressif/mdns", ref="1.8.2") cg.add_define("USE_MDNS") diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index 4648e66e1d..a096408aa5 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -14,49 +14,6 @@ namespace mqtt { static const char *const TAG = "mqtt.idf"; bool MQTTBackendESP32::initialize_() { -#if ESP_IDF_VERSION_MAJOR < 5 - mqtt_cfg_.user_context = (void *) this; - mqtt_cfg_.buffer_size = MQTT_BUFFER_SIZE; - - mqtt_cfg_.host = this->host_.c_str(); - mqtt_cfg_.port = this->port_; - mqtt_cfg_.keepalive = this->keep_alive_; - mqtt_cfg_.disable_clean_session = !this->clean_session_; - - if (!this->username_.empty()) { - mqtt_cfg_.username = this->username_.c_str(); - if (!this->password_.empty()) { - mqtt_cfg_.password = this->password_.c_str(); - } - } - - if (!this->lwt_topic_.empty()) { - mqtt_cfg_.lwt_topic = this->lwt_topic_.c_str(); - this->mqtt_cfg_.lwt_qos = this->lwt_qos_; - this->mqtt_cfg_.lwt_retain = this->lwt_retain_; - - if (!this->lwt_message_.empty()) { - mqtt_cfg_.lwt_msg = this->lwt_message_.c_str(); - mqtt_cfg_.lwt_msg_len = this->lwt_message_.size(); - } - } - - if (!this->client_id_.empty()) { - mqtt_cfg_.client_id = this->client_id_.c_str(); - } - if (ca_certificate_.has_value()) { - mqtt_cfg_.cert_pem = ca_certificate_.value().c_str(); - mqtt_cfg_.skip_cert_common_name_check = skip_cert_cn_check_; - mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_SSL; - - if (this->cl_certificate_.has_value() && this->cl_key_.has_value()) { - mqtt_cfg_.client_cert_pem = this->cl_certificate_.value().c_str(); - mqtt_cfg_.client_key_pem = this->cl_key_.value().c_str(); - } - } else { - mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_TCP; - } -#else mqtt_cfg_.broker.address.hostname = this->host_.c_str(); mqtt_cfg_.broker.address.port = this->port_; mqtt_cfg_.session.keepalive = this->keep_alive_; @@ -95,7 +52,7 @@ bool MQTTBackendESP32::initialize_() { } else { mqtt_cfg_.broker.address.transport = MQTT_TRANSPORT_OVER_TCP; } -#endif + auto *mqtt_client = esp_mqtt_client_init(&mqtt_cfg_); if (mqtt_client) { handler_.reset(mqtt_client); diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp index 49482316ee..b2751470b2 100644 --- a/esphome/components/opentherm/opentherm.cpp +++ b/esphome/components/opentherm/opentherm.cpp @@ -272,18 +272,13 @@ bool OpenTherm::init_esp32_timer_() { this->timer_idx_ = timer_idx; timer_config_t const config = { - .alarm_en = TIMER_ALARM_EN, - .counter_en = TIMER_PAUSE, - .intr_type = TIMER_INTR_LEVEL, - .counter_dir = TIMER_COUNT_UP, - .auto_reload = TIMER_AUTORELOAD_EN, -#if ESP_IDF_VERSION_MAJOR >= 5 - .clk_src = TIMER_SRC_CLK_DEFAULT, -#endif - .divider = 80, -#if defined(SOC_TIMER_GROUP_SUPPORT_XTAL) && ESP_IDF_VERSION_MAJOR < 5 - .clk_src = TIMER_SRC_CLK_APB -#endif + .alarm_en = TIMER_ALARM_EN, + .counter_en = TIMER_PAUSE, + .intr_type = TIMER_INTR_LEVEL, + .counter_dir = TIMER_COUNT_UP, + .auto_reload = TIMER_AUTORELOAD_EN, + .clk_src = TIMER_SRC_CLK_DEFAULT, + .divider = 80, }; esp_err_t result; diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index 6f45fb75e4..cad44a5795 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -6,10 +6,7 @@ #include #include - -#if ESP_IDF_VERSION_MAJOR >= 5 #include -#endif namespace esphome { namespace ota { @@ -24,7 +21,6 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size) { #if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15 // The following function takes longer than the 5 seconds timeout of WDT -#if ESP_IDF_VERSION_MAJOR >= 5 esp_task_wdt_config_t wdtc; wdtc.idle_core_mask = 0; #if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0 @@ -36,21 +32,14 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size) { wdtc.timeout_ms = 15000; wdtc.trigger_panic = false; esp_task_wdt_reconfigure(&wdtc); -#else - esp_task_wdt_init(15, false); -#endif #endif esp_err_t err = esp_ota_begin(this->partition_, image_size, &this->update_handle_); #if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15 // Set the WDT back to the configured timeout -#if ESP_IDF_VERSION_MAJOR >= 5 wdtc.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000; esp_task_wdt_reconfigure(&wdtc); -#else - esp_task_wdt_init(CONFIG_ESP_TASK_WDT_TIMEOUT_S, false); -#endif #endif if (err != ESP_OK) { diff --git a/esphome/components/psram/psram.cpp b/esphome/components/psram/psram.cpp index 162543545e..6c110a577d 100644 --- a/esphome/components/psram/psram.cpp +++ b/esphome/components/psram/psram.cpp @@ -2,9 +2,7 @@ #ifdef USE_ESP32 #include "psram.h" #include -#if defined(USE_ESP_IDF) && ESP_IDF_VERSION_MAJOR >= 5 #include -#endif // USE_ESP_IDF #include "esphome/core/log.h" @@ -16,7 +14,6 @@ static const char *const TAG = "psram"; void PsramComponent::dump_config() { ESP_LOGCONFIG(TAG, "PSRAM:"); -#if defined(USE_ESP_IDF) && ESP_IDF_VERSION_MAJOR >= 5 bool available = esp_psram_is_initialized(); ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available)); @@ -26,23 +23,6 @@ void PsramComponent::dump_config() { ESP_LOGCONFIG(TAG, " ECC enabled: YES"); #endif } -#else - // Technically this can be false if the PSRAM is full, but heap_caps_get_total_size() isn't always available, and it's - // very unlikely for the PSRAM to be full. - bool available = heap_caps_get_free_size(MALLOC_CAP_SPIRAM) > 0; - ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available)); - - if (available) { - const size_t psram_total_size_bytes = heap_caps_get_total_size(MALLOC_CAP_SPIRAM); - const float psram_total_size_kb = psram_total_size_bytes / 1024.0f; - - if (abs(std::round(psram_total_size_kb) - psram_total_size_kb) < 0.05f) { - ESP_LOGCONFIG(TAG, " Size: %.0f KB", psram_total_size_kb); - } else { - ESP_LOGCONFIG(TAG, " Size: %zu bytes", psram_total_size_bytes); - } - } -#endif // USE_ESP_IDF } } // namespace psram diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp index 74f63a9640..4b6c11b332 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp @@ -146,11 +146,7 @@ void PVVXDisplay::sync_time_() { } time.recalc_timestamp_utc(true); // calculate timestamp of local time uint8_t blk[5] = {}; -#if ESP_IDF_VERSION_MAJOR >= 5 ESP_LOGD(TAG, "[%s] Sync time with timestamp %" PRIu64 ".", this->parent_->address_str().c_str(), time.timestamp); -#else - ESP_LOGD(TAG, "[%s] Sync time with timestamp %lu.", this->parent_->address_str().c_str(), time.timestamp); -#endif blk[0] = 0x23; blk[1] = time.timestamp & 0xff; blk[2] = (time.timestamp >> 8) & 0xff; diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp index 666bac354d..91eb947a3e 100644 --- a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp +++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp @@ -10,10 +10,8 @@ void RpiDpiRgb::setup() { this->reset_display_(); esp_lcd_rgb_panel_config_t config{}; config.flags.fb_in_psram = 1; -#if ESP_IDF_VERSION_MAJOR >= 5 config.bounce_buffer_size_px = this->width_ * 10; config.num_fbs = 1; -#endif // ESP_IDF_VERSION_MAJOR config.timings.h_res = this->width_; config.timings.v_res = this->height_; config.timings.hsync_pulse_width = this->hsync_pulse_width_; @@ -47,10 +45,8 @@ void RpiDpiRgb::setup() { ESP_LOGCONFIG(TAG, "RPI_DPI_RGB setup complete"); } void RpiDpiRgb::loop() { -#if ESP_IDF_VERSION_MAJOR >= 5 if (this->handle_ != nullptr) esp_lcd_rgb_panel_restart(this->handle_); -#endif // ESP_IDF_VERSION_MAJOR } void RpiDpiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, diff --git a/esphome/components/st7701s/st7701s.cpp b/esphome/components/st7701s/st7701s.cpp index 6261f33b77..46509a7f9f 100644 --- a/esphome/components/st7701s/st7701s.cpp +++ b/esphome/components/st7701s/st7701s.cpp @@ -12,10 +12,8 @@ void ST7701S::setup() { esp_lcd_rgb_panel_config_t config{}; config.flags.fb_in_psram = 1; -#if ESP_IDF_VERSION_MAJOR >= 5 config.bounce_buffer_size_px = this->width_ * 10; config.num_fbs = 1; -#endif // ESP_IDF_VERSION_MAJOR config.timings.h_res = this->width_; config.timings.v_res = this->height_; config.timings.hsync_pulse_width = this->hsync_pulse_width_; @@ -48,10 +46,8 @@ void ST7701S::setup() { } void ST7701S::loop() { -#if ESP_IDF_VERSION_MAJOR >= 5 if (this->handle_ != nullptr) esp_lcd_rgb_panel_restart(this->handle_); -#endif // ESP_IDF_VERSION_MAJOR } void ST7701S::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 8fae63a603..63b2579c3f 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -48,11 +48,7 @@ uart_config_t IDFUARTComponent::get_config_() { uart_config.parity = parity; uart_config.stop_bits = this->stop_bits_ == 1 ? UART_STOP_BITS_1 : UART_STOP_BITS_2; uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) uart_config.source_clk = UART_SCLK_DEFAULT; -#else - uart_config.source_clk = UART_SCLK_APB; -#endif uart_config.rx_flow_ctrl_thresh = 122; return uart_config; diff --git a/esphome/components/watchdog/watchdog.cpp b/esphome/components/watchdog/watchdog.cpp index f6f2992a11..2ce46756e4 100644 --- a/esphome/components/watchdog/watchdog.cpp +++ b/esphome/components/watchdog/watchdog.cpp @@ -38,16 +38,12 @@ WatchdogManager::~WatchdogManager() { void WatchdogManager::set_timeout_(uint32_t timeout_ms) { ESP_LOGV(TAG, "Adjusting WDT to %" PRIu32 "ms", timeout_ms); #ifdef USE_ESP32 -#if ESP_IDF_VERSION_MAJOR >= 5 esp_task_wdt_config_t wdt_config = { .timeout_ms = timeout_ms, .idle_core_mask = (1 << SOC_CPU_CORES_NUM) - 1, .trigger_panic = true, }; esp_task_wdt_reconfigure(&wdt_config); -#else - esp_task_wdt_init(timeout_ms / 1000, true); -#endif // ESP_IDF_VERSION_MAJOR #endif // USE_ESP32 #ifdef USE_RP2040 diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py index fc0e4e0538..8eff8e7b2a 100644 --- a/esphome/components/wireguard/__init__.py +++ b/esphome/components/wireguard/__init__.py @@ -6,14 +6,7 @@ import esphome.codegen as cg from esphome.components import time from esphome.components.esp32 import CORE, add_idf_sdkconfig_option import esphome.config_validation as cv -from esphome.const import ( - CONF_ADDRESS, - CONF_ID, - CONF_REBOOT_TIMEOUT, - CONF_TIME_ID, - KEY_CORE, - KEY_FRAMEWORK_VERSION, -) +from esphome.const import CONF_ADDRESS, CONF_ID, CONF_REBOOT_TIMEOUT, CONF_TIME_ID from esphome.core import TimePeriod CONF_NETMASK = "netmask" @@ -125,9 +118,7 @@ async def to_code(config): # Workaround for crash on IDF 5+ # See https://github.com/trombik/esp_wireguard/issues/33#issuecomment-1568503651 - if CORE.using_esp_idf and CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version( - 5, 0, 0 - ): + if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_LWIP_PPP_SUPPORT", True) # This flag is added here because the esp_wireguard library statically From 0083abe3b5ad93192d2ab33bf16660dc286f982e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Jul 2025 18:30:03 -0500 Subject: [PATCH 198/293] Fix regression: BK7231N devices not returning entities via API (#9283) --- esphome/components/api/api_connection.cpp | 47 ++++----- esphome/components/api/api_connection.h | 119 ++++++++++------------ 2 files changed, 72 insertions(+), 94 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index b7624221c9..e83d508c50 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1687,7 +1687,9 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c // O(n) but optimized for RAM and not performance. for (auto &item : items) { if (item.entity == entity && item.message_type == message_type) { - // Update the existing item with the new creator + // Clean up old creator before replacing + item.creator.cleanup(message_type); + // Move assign the new creator item.creator = std::move(creator); return; } @@ -1730,11 +1732,11 @@ void APIConnection::process_batch_() { return; } - size_t num_items = this->deferred_batch_.items.size(); + size_t num_items = this->deferred_batch_.size(); // Fast path for single message - allocate exact size needed if (num_items == 1) { - const auto &item = this->deferred_batch_.items[0]; + const auto &item = this->deferred_batch_[0]; // Let the creator calculate size and encode if it fits uint16_t payload_size = @@ -1764,7 +1766,8 @@ void APIConnection::process_batch_() { // Pre-calculate exact buffer size needed based on message types uint32_t total_estimated_size = 0; - for (const auto &item : this->deferred_batch_.items) { + for (size_t i = 0; i < this->deferred_batch_.size(); i++) { + const auto &item = this->deferred_batch_[i]; total_estimated_size += get_estimated_message_size(item.message_type); } @@ -1785,7 +1788,8 @@ void APIConnection::process_batch_() { uint32_t current_offset = 0; // Process items and encode directly to buffer - for (const auto &item : this->deferred_batch_.items) { + for (size_t i = 0; i < this->deferred_batch_.size(); i++) { + const auto &item = this->deferred_batch_[i]; // Try to encode message // The creator will calculate overhead to determine if the message fits uint16_t payload_size = item.creator(item.entity, this, remaining_size, false, item.message_type); @@ -1840,17 +1844,15 @@ void APIConnection::process_batch_() { // Log messages after send attempt for VV debugging // It's safe to use the buffer for logging at this point regardless of send result for (size_t i = 0; i < items_processed; i++) { - const auto &item = this->deferred_batch_.items[i]; + const auto &item = this->deferred_batch_[i]; this->log_batch_item_(item); } #endif // Handle remaining items more efficiently - if (items_processed < this->deferred_batch_.items.size()) { - // Remove processed items from the beginning - this->deferred_batch_.items.erase(this->deferred_batch_.items.begin(), - this->deferred_batch_.items.begin() + items_processed); - + if (items_processed < this->deferred_batch_.size()) { + // Remove processed items from the beginning with proper cleanup + this->deferred_batch_.remove_front(items_processed); // Reschedule for remaining items this->schedule_batch_(); } else { @@ -1861,23 +1863,16 @@ void APIConnection::process_batch_() { uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, uint16_t message_type) const { - if (has_tagged_string_ptr_()) { - // Handle string-based messages - switch (message_type) { #ifdef USE_EVENT - case EventResponse::MESSAGE_TYPE: { - auto *e = static_cast(entity); - return APIConnection::try_send_event_response(e, *get_string_ptr_(), conn, remaining_size, is_single); - } -#endif - default: - // Should not happen, return 0 to indicate no message - return 0; - } - } else { - // Function pointer case - return data_.ptr(entity, conn, remaining_size, is_single); + // Special case: EventResponse uses string pointer + if (message_type == EventResponse::MESSAGE_TYPE) { + auto *e = static_cast(entity); + return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single); } +#endif + + // All other message types use function pointers + return data_.function_ptr(entity, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 410a9ad3a5..151369aa70 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -451,96 +451,53 @@ class APIConnection : public APIServerConnection { // Function pointer type for message encoding using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); - // Optimized MessageCreator class using tagged pointer class MessageCreator { - // Ensure pointer alignment allows LSB tagging - static_assert(alignof(std::string *) > 1, "String pointer alignment must be > 1 for LSB tagging"); - public: // Constructor for function pointer - MessageCreator(MessageCreatorPtr ptr) { - // Function pointers are always aligned, so LSB is 0 - data_.ptr = ptr; - } + MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; } // Constructor for string state capture - explicit MessageCreator(const std::string &str_value) { - // Allocate string and tag the pointer - auto *str = new std::string(str_value); - // Set LSB to 1 to indicate string pointer - data_.tagged = reinterpret_cast(str) | 1; - } + explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); } - // Destructor - ~MessageCreator() { - if (has_tagged_string_ptr_()) { - delete get_string_ptr_(); - } - } + // No destructor - cleanup must be called explicitly with message_type - // Copy constructor - MessageCreator(const MessageCreator &other) { - if (other.has_tagged_string_ptr_()) { - auto *str = new std::string(*other.get_string_ptr_()); - data_.tagged = reinterpret_cast(str) | 1; - } else { - data_ = other.data_; - } - } + // Delete copy operations - MessageCreator should only be moved + MessageCreator(const MessageCreator &other) = delete; + MessageCreator &operator=(const MessageCreator &other) = delete; // Move constructor - MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.ptr = nullptr; } - - // Assignment operators (needed for batch deduplication) - MessageCreator &operator=(const MessageCreator &other) { - if (this != &other) { - // Clean up current string data if needed - if (has_tagged_string_ptr_()) { - delete get_string_ptr_(); - } - // Copy new data - if (other.has_tagged_string_ptr_()) { - auto *str = new std::string(*other.get_string_ptr_()); - data_.tagged = reinterpret_cast(str) | 1; - } else { - data_ = other.data_; - } - } - return *this; - } + MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.function_ptr = nullptr; } + // Move assignment MessageCreator &operator=(MessageCreator &&other) noexcept { if (this != &other) { - // Clean up current string data if needed - if (has_tagged_string_ptr_()) { - delete get_string_ptr_(); - } - // Move data + // IMPORTANT: Caller must ensure cleanup() was called if this contains a string! + // In our usage, this happens in add_item() deduplication and vector::erase() data_ = other.data_; - // Reset other to safe state - other.data_.ptr = nullptr; + other.data_.function_ptr = nullptr; } return *this; } - // Call operator - now accepts message_type as parameter + // Call operator - uses message_type to determine union type uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, uint16_t message_type) const; - private: - // Check if this contains a string pointer - bool has_tagged_string_ptr_() const { return (data_.tagged & 1) != 0; } - - // Get the actual string pointer (clears the tag bit) - std::string *get_string_ptr_() const { - // NOLINTNEXTLINE(performance-no-int-to-ptr) - return reinterpret_cast(data_.tagged & ~uintptr_t(1)); + // Manual cleanup method - must be called before destruction for string types + void cleanup(uint16_t message_type) { +#ifdef USE_EVENT + if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) { + delete data_.string_ptr; + data_.string_ptr = nullptr; + } +#endif } - union { - MessageCreatorPtr ptr; - uintptr_t tagged; - } data_; // 4 bytes on 32-bit + private: + union Data { + MessageCreatorPtr function_ptr; + std::string *string_ptr; + } data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before }; // Generic batching mechanism for both state updates and entity info @@ -558,20 +515,46 @@ class APIConnection : public APIServerConnection { std::vector items; uint32_t batch_start_time{0}; + private: + // Helper to cleanup items from the beginning + void cleanup_items_(size_t count) { + for (size_t i = 0; i < count; i++) { + items[i].creator.cleanup(items[i].message_type); + } + } + + public: DeferredBatch() { // Pre-allocate capacity for typical batch sizes to avoid reallocation items.reserve(8); } + ~DeferredBatch() { + // Ensure cleanup of any remaining items + clear(); + } + // Add item to the batch void add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type); // Add item to the front of the batch (for high priority messages like ping) void add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type); + + // Clear all items with proper cleanup void clear() { + cleanup_items_(items.size()); items.clear(); batch_start_time = 0; } + + // Remove processed items from the front with proper cleanup + void remove_front(size_t count) { + cleanup_items_(count); + items.erase(items.begin(), items.begin() + count); + } + bool empty() const { return items.empty(); } + size_t size() const { return items.size(); } + const BatchItem &operator[](size_t index) const { return items[index]; } }; // DeferredBatch here (16 bytes, 4-byte aligned) From 04a46de23794bffe17c8094cdc8534c01cd61906 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 1 Jul 2025 19:40:39 -0400 Subject: [PATCH 199/293] [esp32_rmt_led_strip] Reduce memory usage by 32x with IDF 5.3 (#8388) --- .../esp32_rmt_led_strip/led_strip.cpp | 95 +++++++++++++++---- .../esp32_rmt_led_strip/led_strip.h | 13 ++- 2 files changed, 89 insertions(+), 19 deletions(-) diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index dfdf50aa66..389c32882b 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -21,6 +21,43 @@ static const uint32_t RMT_CLK_FREQ = 80000000; static const uint8_t RMT_CLK_DIV = 2; #endif +static const size_t RMT_SYMBOLS_PER_BYTE = 8; + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) +static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t symbols_written, size_t symbols_free, + rmt_symbol_word_t *symbols, bool *done, void *arg) { + auto *params = static_cast(arg); + const auto *bytes = static_cast(data); + size_t index = symbols_written / RMT_SYMBOLS_PER_BYTE; + + // convert byte to symbols + if (index < size) { + if (symbols_free < RMT_SYMBOLS_PER_BYTE) { + return 0; + } + for (int32_t i = 0; i < RMT_SYMBOLS_PER_BYTE; i++) { + if (bytes[index] & (1 << (7 - i))) { + symbols[i] = params->bit1; + } else { + symbols[i] = params->bit0; + } + } + if ((index + 1) >= size && params->reset.duration0 == 0 && params->reset.duration1 == 0) { + *done = true; + } + return RMT_SYMBOLS_PER_BYTE; + } + + // send reset + if (symbols_free < 1) { + return 0; + } + symbols[0] = params->reset; + *done = true; + return 1; +} +#endif + void ESP32RMTLEDStripLightOutput::setup() { ESP_LOGCONFIG(TAG, "Running setup"); @@ -42,10 +79,15 @@ void ESP32RMTLEDStripLightOutput::setup() { return; } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + // copy of the led buffer + this->rmt_buf_ = allocator.allocate(buffer_size); +#else RAMAllocator rmt_allocator(this->use_psram_ ? 0 : RAMAllocator::ALLOC_INTERNAL); // 8 bits per byte, 1 rmt_symbol_word_t per bit + 1 rmt_symbol_word_t for reset this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + 1); +#endif rmt_tx_channel_config_t channel; memset(&channel, 0, sizeof(channel)); @@ -65,6 +107,18 @@ void ESP32RMTLEDStripLightOutput::setup() { return; } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + rmt_simple_encoder_config_t encoder; + memset(&encoder, 0, sizeof(encoder)); + encoder.callback = encoder_callback; + encoder.arg = &this->params_; + encoder.min_chunk_size = 8; + if (rmt_new_simple_encoder(&encoder, &this->encoder_) != ESP_OK) { + ESP_LOGE(TAG, "Encoder creation failed"); + this->mark_failed(); + return; + } +#else rmt_copy_encoder_config_t encoder; memset(&encoder, 0, sizeof(encoder)); if (rmt_new_copy_encoder(&encoder, &this->encoder_) != ESP_OK) { @@ -72,6 +126,7 @@ void ESP32RMTLEDStripLightOutput::setup() { this->mark_failed(); return; } +#endif if (rmt_enable(this->channel_) != ESP_OK) { ESP_LOGE(TAG, "Enabling channel failed"); @@ -85,20 +140,20 @@ void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bi float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f; // 0-bit - this->bit0_.duration0 = (uint32_t) (ratio * bit0_high); - this->bit0_.level0 = 1; - this->bit0_.duration1 = (uint32_t) (ratio * bit0_low); - this->bit0_.level1 = 0; + this->params_.bit0.duration0 = (uint32_t) (ratio * bit0_high); + this->params_.bit0.level0 = 1; + this->params_.bit0.duration1 = (uint32_t) (ratio * bit0_low); + this->params_.bit0.level1 = 0; // 1-bit - this->bit1_.duration0 = (uint32_t) (ratio * bit1_high); - this->bit1_.level0 = 1; - this->bit1_.duration1 = (uint32_t) (ratio * bit1_low); - this->bit1_.level1 = 0; + this->params_.bit1.duration0 = (uint32_t) (ratio * bit1_high); + this->params_.bit1.level0 = 1; + this->params_.bit1.duration1 = (uint32_t) (ratio * bit1_low); + this->params_.bit1.level1 = 0; // reset - this->reset_.duration0 = (uint32_t) (ratio * reset_time_high); - this->reset_.level0 = 1; - this->reset_.duration1 = (uint32_t) (ratio * reset_time_low); - this->reset_.level1 = 0; + this->params_.reset.duration0 = (uint32_t) (ratio * reset_time_high); + this->params_.reset.level0 = 1; + this->params_.reset.duration1 = (uint32_t) (ratio * reset_time_low); + this->params_.reset.level1 = 0; } void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { @@ -122,6 +177,9 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { } delayMicroseconds(50); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + memcpy(this->rmt_buf_, this->buf_, this->get_buffer_size_()); +#else size_t buffer_size = this->get_buffer_size_(); size_t size = 0; @@ -131,7 +189,7 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { while (size < buffer_size) { uint8_t b = *psrc; for (int i = 0; i < 8; i++) { - pdest->val = b & (1 << (7 - i)) ? this->bit1_.val : this->bit0_.val; + pdest->val = b & (1 << (7 - i)) ? this->params_.bit1.val : this->params_.bit0.val; pdest++; len++; } @@ -139,17 +197,20 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { psrc++; } - if (this->reset_.duration0 > 0 || this->reset_.duration1 > 0) { - pdest->val = this->reset_.val; + if (this->params_.reset.duration0 > 0 || this->params_.reset.duration1 > 0) { + pdest->val = this->params_.reset.val; pdest++; len++; } +#endif rmt_transmit_config_t config; memset(&config, 0, sizeof(config)); - config.loop_count = 0; - config.flags.eot_level = 0; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, this->get_buffer_size_(), &config); +#else error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, len * sizeof(rmt_symbol_word_t), &config); +#endif if (error != ESP_OK) { ESP_LOGE(TAG, "RMT TX error"); this->status_set_warning(); diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.h b/esphome/components/esp32_rmt_led_strip/led_strip.h index c6a2b4bc9f..72ce659b4f 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.h +++ b/esphome/components/esp32_rmt_led_strip/led_strip.h @@ -25,6 +25,12 @@ enum RGBOrder : uint8_t { ORDER_BRG, }; +struct LedParams { + rmt_symbol_word_t bit0; + rmt_symbol_word_t bit1; + rmt_symbol_word_t reset; +}; + class ESP32RMTLEDStripLightOutput : public light::AddressableLight { public: void setup() override; @@ -72,12 +78,15 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { uint8_t *buf_{nullptr}; uint8_t *effect_data_{nullptr}; + LedParams params_; rmt_channel_handle_t channel_{nullptr}; rmt_encoder_handle_t encoder_{nullptr}; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + uint8_t *rmt_buf_{nullptr}; +#else rmt_symbol_word_t *rmt_buf_{nullptr}; - rmt_symbol_word_t bit0_, bit1_, reset_; +#endif uint32_t rmt_symbols_{48}; - uint8_t pin_; uint16_t num_leds_; bool is_rgbw_{false}; From 6a096c1d5a734ed3f9db5f70c3fecdaec55dc777 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 2 Jul 2025 13:36:15 +1200 Subject: [PATCH 200/293] [api] Dump bytes fields as hex instead of unreadable string (#9288) --- esphome/components/api/api_pb2.cpp | 23 ++++++++++++----------- esphome/core/helpers.cpp | 21 ++++++++++++++++++--- esphome/core/helpers.h | 2 ++ script/api_protobuf/api_protobuf.py | 3 ++- 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 9793565ee5..8bce14c9cc 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3,6 +3,7 @@ #include "api_pb2.h" #include "api_pb2_size.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" #include @@ -3510,7 +3511,7 @@ void SubscribeLogsResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" message: "); - out.append("'").append(this->message).append("'"); + out.append(format_hex_pretty(this->message)); out.append("\n"); out.append(" send_failed: "); @@ -3538,7 +3539,7 @@ void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("NoiseEncryptionSetKeyRequest {\n"); out.append(" key: "); - out.append("'").append(this->key).append("'"); + out.append(format_hex_pretty(this->key)); out.append("\n"); out.append("}"); } @@ -4284,7 +4285,7 @@ void CameraImageResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" data: "); - out.append("'").append(this->data).append("'"); + out.append(format_hex_pretty(this->data)); out.append("\n"); out.append(" done: "); @@ -6811,7 +6812,7 @@ void BluetoothServiceData::dump_to(std::string &out) const { } out.append(" data: "); - out.append("'").append(this->data).append("'"); + out.append(format_hex_pretty(this->data)); out.append("\n"); out.append("}"); } @@ -6894,7 +6895,7 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + out.append(format_hex_pretty(this->name)); out.append("\n"); out.append(" rssi: "); @@ -6987,7 +6988,7 @@ void BluetoothLERawAdvertisement::dump_to(std::string &out) const { out.append("\n"); out.append(" data: "); - out.append("'").append(this->data).append("'"); + out.append(format_hex_pretty(this->data)); out.append("\n"); out.append("}"); } @@ -7514,7 +7515,7 @@ void BluetoothGATTReadResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" data: "); - out.append("'").append(this->data).append("'"); + out.append(format_hex_pretty(this->data)); out.append("\n"); out.append("}"); } @@ -7578,7 +7579,7 @@ void BluetoothGATTWriteRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" data: "); - out.append("'").append(this->data).append("'"); + out.append(format_hex_pretty(this->data)); out.append("\n"); out.append("}"); } @@ -7670,7 +7671,7 @@ void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" data: "); - out.append("'").append(this->data).append("'"); + out.append(format_hex_pretty(this->data)); out.append("\n"); out.append("}"); } @@ -7772,7 +7773,7 @@ void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" data: "); - out.append("'").append(this->data).append("'"); + out.append(format_hex_pretty(this->data)); out.append("\n"); out.append("}"); } @@ -8492,7 +8493,7 @@ void VoiceAssistantAudio::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("VoiceAssistantAudio {\n"); out.append(" data: "); - out.append("'").append(this->data).append("'"); + out.append(format_hex_pretty(this->data)); out.append("\n"); out.append(" end: "); diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index fc91d83972..b4923c7af0 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -4,13 +4,13 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include #include #include #include #include #include #include -#include #ifdef USE_HOST #ifndef _WIN32 @@ -43,10 +43,10 @@ #include #endif #ifdef USE_ESP32 -#include "rom/crc.h" -#include "esp_mac.h" #include "esp_efuse.h" #include "esp_efuse_table.h" +#include "esp_mac.h" +#include "rom/crc.h" #endif #ifdef USE_LIBRETINY @@ -393,6 +393,21 @@ std::string format_hex_pretty(const uint16_t *data, size_t length) { return ret; } std::string format_hex_pretty(const std::vector &data) { return format_hex_pretty(data.data(), data.size()); } +std::string format_hex_pretty(const std::string &data) { + if (data.empty()) + return ""; + std::string ret; + ret.resize(3 * data.length() - 1); + for (size_t i = 0; i < data.length(); i++) { + ret[3 * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4); + ret[3 * i + 1] = format_hex_pretty_char(data[i] & 0x0F); + if (i != data.length() - 1) + ret[3 * i + 2] = '.'; + } + if (data.length() > 4) + return ret + " (" + std::to_string(data.length()) + ")"; + return ret; +} std::string format_bin(const uint8_t *data, size_t length) { std::string result; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 7d5366f323..362f3d1fa4 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -348,6 +348,8 @@ std::string format_hex_pretty(const uint16_t *data, size_t length); std::string format_hex_pretty(const std::vector &data); /// Format the vector \p data in pretty-printed, human-readable hex. std::string format_hex_pretty(const std::vector &data); +/// Format the string \p data in pretty-printed, human-readable hex. +std::string format_hex_pretty(const std::string &data); /// Format an unsigned integer in pretty-printed, human-readable hex, starting with the most significant byte. template::value, int> = 0> std::string format_hex_pretty(T val) { val = convert_big_endian(val); diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index ad8e41ba5e..615f5bbfda 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -530,7 +530,7 @@ class BytesType(TypeInfo): wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2 def dump(self, name: str) -> str: - o = f'out.append("\'").append({name}).append("\'");' + o = f"out.append(format_hex_pretty({name}));" return o def get_size_calculation(self, name: str, force: bool = False) -> str: @@ -1255,6 +1255,7 @@ def main() -> None: #include "api_pb2.h" #include "api_pb2_size.h" #include "esphome/core/log.h" + #include "esphome/core/helpers.h" #include From 03566c34ed014d6babad37dfc3cbe6fb4772e214 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Jul 2025 20:43:40 -0500 Subject: [PATCH 201/293] Reduce Component memory usage by 40% (8 bytes per component) (#9278) --- esphome/core/application.cpp | 4 ++ esphome/core/component.cpp | 94 +++++++++++++++++++++++++++++++++--- esphome/core/component.h | 5 +- 3 files changed, 93 insertions(+), 10 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 1599c648e7..d6fab018cc 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -84,6 +84,10 @@ void Application::setup() { } ESP_LOGI(TAG, "setup() finished successfully!"); + + // Clear setup priority overrides to free memory + clear_setup_priority_overrides(); + this->schedule_dump_config(); this->calculate_looping_components_(); } diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 6661223e35..aba5dc729c 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -2,7 +2,9 @@ #include #include +#include #include +#include #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -12,6 +14,30 @@ namespace esphome { static const char *const TAG = "component"; +// Global vectors for component data that doesn't belong in every instance. +// Using vector instead of unordered_map for both because: +// - Much lower memory overhead (8 bytes per entry vs 20+ for unordered_map) +// - Linear search is fine for small n (typically < 5 entries) +// - These are rarely accessed (setup only or error cases only) + +// Component error messages - only stores messages for failed components +// Lazy allocated since most configs have zero failures +// Note: We don't clear this vector because: +// 1. Components are never destroyed in ESPHome +// 2. Failed components remain failed (no recovery mechanism) +// 3. Memory usage is minimal (only failures with custom messages are stored) +static std::unique_ptr>> &get_component_error_messages() { + static std::unique_ptr>> instance; + return instance; +} + +// Setup priority overrides - freed after setup completes +// Typically < 5 entries, lazy allocated +static std::unique_ptr>> &get_setup_priority_overrides() { + static std::unique_ptr>> instance; + return instance; +} + namespace setup_priority { const float BUS = 1000.0f; @@ -102,8 +128,17 @@ void Component::call_setup() { this->setup(); } void Component::call_dump_config() { this->dump_config(); if (this->is_failed()) { - ESP_LOGE(TAG, " Component %s is marked FAILED: %s", this->get_component_source(), - this->error_message_ ? this->error_message_ : "unspecified"); + // Look up error message from global vector + const char *error_msg = "unspecified"; + if (get_component_error_messages()) { + for (const auto &pair : *get_component_error_messages()) { + if (pair.first == this) { + error_msg = pair.second; + break; + } + } + } + ESP_LOGE(TAG, " Component %s is marked FAILED: %s", this->get_component_source(), error_msg); } } @@ -245,8 +280,21 @@ void Component::status_set_error(const char *message) { this->component_state_ |= STATUS_LED_ERROR; App.app_state_ |= STATUS_LED_ERROR; ESP_LOGE(TAG, "Component %s set Error flag: %s", this->get_component_source(), message); - if (strcmp(message, "unspecified") != 0) - this->error_message_ = message; + if (strcmp(message, "unspecified") != 0) { + // Lazy allocate the error messages vector if needed + if (!get_component_error_messages()) { + get_component_error_messages() = std::make_unique>>(); + } + // Check if this component already has an error message + for (auto &pair : *get_component_error_messages()) { + if (pair.first == this) { + pair.second = message; + return; + } + } + // Add new error message + get_component_error_messages()->emplace_back(this, message); + } } void Component::status_clear_warning() { if ((this->component_state_ & STATUS_LED_WARNING) == 0) @@ -270,11 +318,36 @@ void Component::status_momentary_error(const std::string &name, uint32_t length) } void Component::dump_config() {} float Component::get_actual_setup_priority() const { - if (std::isnan(this->setup_priority_override_)) - return this->get_setup_priority(); - return this->setup_priority_override_; + // Check if there's an override in the global vector + if (get_setup_priority_overrides()) { + // Linear search is fine for small n (typically < 5 overrides) + for (const auto &pair : *get_setup_priority_overrides()) { + if (pair.first == this) { + return pair.second; + } + } + } + return this->get_setup_priority(); +} +void Component::set_setup_priority(float priority) { + // Lazy allocate the vector if needed + if (!get_setup_priority_overrides()) { + get_setup_priority_overrides() = std::make_unique>>(); + // Reserve some space to avoid reallocations (most configs have < 10 overrides) + get_setup_priority_overrides()->reserve(10); + } + + // Check if this component already has an override + for (auto &pair : *get_setup_priority_overrides()) { + if (pair.first == this) { + pair.second = priority; + return; + } + } + + // Add new override + get_setup_priority_overrides()->emplace_back(this, priority); } -void Component::set_setup_priority(float priority) { this->setup_priority_override_ = priority; } bool Component::has_overridden_loop() const { #if defined(USE_HOST) || defined(CLANG_TIDY) @@ -336,4 +409,9 @@ uint32_t WarnIfComponentBlockingGuard::finish() { WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {} +void clear_setup_priority_overrides() { + // Free the setup priority map completely + get_setup_priority_overrides().reset(); +} + } // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h index 5b37deeb68..ab30466e2d 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -387,9 +387,7 @@ class Component { bool cancel_defer(const std::string &name); // NOLINT // Ordered for optimal packing on 32-bit systems - float setup_priority_override_{NAN}; const char *component_source_{nullptr}; - const char *error_message_{nullptr}; uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s) /// State of this component - each bit has a purpose: /// Bits 0-1: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED) @@ -459,4 +457,7 @@ class WarnIfComponentBlockingGuard { Component *component_; }; +// Function to clear setup priority overrides after all components are set up +void clear_setup_priority_overrides(); + } // namespace esphome From 84ab758b227c014d001a1736b7ed7745f892fc96 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Jul 2025 20:50:45 -0500 Subject: [PATCH 202/293] Replace custom OTA implementation in web_server_base (#9274) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/captive_portal/__init__.py | 2 +- .../captive_portal/captive_portal.cpp | 3 - esphome/components/ota/ota_backend.h | 28 +- .../ota/ota_backend_arduino_esp32.cpp | 14 +- .../ota/ota_backend_arduino_esp32.h | 3 + .../ota/ota_backend_arduino_esp8266.cpp | 22 +- .../ota/ota_backend_arduino_esp8266.h | 3 + .../ota/ota_backend_arduino_libretiny.cpp | 14 +- .../ota/ota_backend_arduino_libretiny.h | 3 + .../ota/ota_backend_arduino_rp2040.cpp | 11 +- .../ota/ota_backend_arduino_rp2040.h | 3 + .../components/ota/ota_backend_esp_idf.cpp | 15 +- esphome/components/ota/ota_backend_esp_idf.h | 1 + esphome/components/web_server/__init__.py | 35 ++- esphome/components/web_server/ota/__init__.py | 32 +++ .../web_server/ota/ota_web_server.cpp | 210 ++++++++++++++ .../web_server/ota/ota_web_server.h | 26 ++ esphome/components/web_server/web_server.cpp | 11 +- esphome/components/web_server/web_server.h | 6 - .../components/web_server/web_server_v1.cpp | 9 +- .../components/web_server_base/__init__.py | 1 + .../web_server_base/web_server_base.cpp | 264 +----------------- .../web_server_base/web_server_base.h | 42 +-- esphome/components/web_server_idf/__init__.py | 8 +- esphome/config.py | 109 ++++++-- .../ota/test_web_server_ota.py | 102 +++++++ .../ota/test_web_server_ota.yaml | 15 + .../ota/test_web_server_ota_arduino.yaml | 18 ++ .../ota/test_web_server_ota_callbacks.yaml | 31 ++ .../ota/test_web_server_ota_esp8266.yaml | 15 + .../ota/test_web_server_ota_idf.yaml | 17 ++ .../ota/test_web_server_ota_multi.yaml | 21 ++ .../web_server/test_ota_migration.py | 38 +++ .../web_server/test_no_ota.esp32-idf.yaml | 9 +- .../web_server/test_ota.esp32-idf.yaml | 8 +- .../test_ota_disabled.esp32-idf.yaml | 11 +- 37 files changed, 776 insertions(+), 385 deletions(-) create mode 100644 esphome/components/web_server/ota/__init__.py create mode 100644 esphome/components/web_server/ota/ota_web_server.cpp create mode 100644 esphome/components/web_server/ota/ota_web_server.h create mode 100644 tests/component_tests/ota/test_web_server_ota.py create mode 100644 tests/component_tests/ota/test_web_server_ota.yaml create mode 100644 tests/component_tests/ota/test_web_server_ota_arduino.yaml create mode 100644 tests/component_tests/ota/test_web_server_ota_callbacks.yaml create mode 100644 tests/component_tests/ota/test_web_server_ota_esp8266.yaml create mode 100644 tests/component_tests/ota/test_web_server_ota_idf.yaml create mode 100644 tests/component_tests/ota/test_web_server_ota_multi.yaml create mode 100644 tests/component_tests/web_server/test_ota_migration.py diff --git a/CODEOWNERS b/CODEOWNERS index 68c8684024..16f38da725 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -498,6 +498,7 @@ esphome/components/voice_assistant/* @jesserockz @kahrendt esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 esphome/components/watchdog/* @oarcher esphome/components/waveshare_epaper/* @clydebarrow +esphome/components/web_server/ota/* @esphome/core esphome/components/web_server_base/* @OttoWinter esphome/components/web_server_idf/* @dentra esphome/components/weikai/* @DrCoolZic diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index cba3b4921a..7e8afd8fab 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -12,7 +12,7 @@ from esphome.const import ( ) from esphome.core import CORE, coroutine_with_priority -AUTO_LOAD = ["web_server_base"] +AUTO_LOAD = ["web_server_base", "ota.web_server"] DEPENDENCIES = ["wifi"] CODEOWNERS = ["@OttoWinter"] diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index ba392bb0f2..25179fdacc 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -47,9 +47,6 @@ void CaptivePortal::start() { this->base_->init(); if (!this->initialized_) { this->base_->add_handler(this); -#ifdef USE_WEBSERVER_OTA - this->base_->add_ota_handler(); -#endif } #ifdef USE_ARDUINO diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index bc8ab46643..372f24df5e 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -67,7 +67,28 @@ class OTAComponent : public Component { } protected: - CallbackManager state_callback_{}; + /** Extended callback manager with deferred call support. + * + * This adds a call_deferred() method for thread-safe execution from other tasks. + */ + class StateCallbackManager : public CallbackManager { + public: + StateCallbackManager(OTAComponent *component) : component_(component) {} + + /** Call callbacks with deferral to main loop (for thread safety). + * + * This should be used by OTA implementations that run in separate tasks + * (like web_server OTA) to ensure callbacks execute in the main loop. + */ + void call_deferred(ota::OTAState state, float progress, uint8_t error) { + component_->defer([this, state, progress, error]() { this->call(state, progress, error); }); + } + + private: + OTAComponent *component_; + }; + + StateCallbackManager state_callback_{this}; #endif }; @@ -89,6 +110,11 @@ class OTAGlobalCallback { OTAGlobalCallback *get_global_ota_callback(); void register_ota_platform(OTAComponent *ota_caller); + +// OTA implementations should use: +// - state_callback_.call() when already in main loop (e.g., esphome OTA) +// - state_callback_.call_deferred() when in separate task (e.g., web_server OTA) +// This ensures proper callback execution in all contexts. #endif std::unique_ptr make_ota_backend(); diff --git a/esphome/components/ota/ota_backend_arduino_esp32.cpp b/esphome/components/ota/ota_backend_arduino_esp32.cpp index 15dfc98a6c..5c6230f2ce 100644 --- a/esphome/components/ota/ota_backend_arduino_esp32.cpp +++ b/esphome/components/ota/ota_backend_arduino_esp32.cpp @@ -15,6 +15,11 @@ static const char *const TAG = "ota.arduino_esp32"; std::unique_ptr make_ota_backend() { return make_unique(); } OTAResponseTypes ArduinoESP32OTABackend::begin(size_t image_size) { + // Handle UPDATE_SIZE_UNKNOWN (0) which is used by web server OTA + // where the exact firmware size is unknown due to multipart encoding + if (image_size == 0) { + image_size = UPDATE_SIZE_UNKNOWN; + } bool ret = Update.begin(image_size, U_FLASH); if (ret) { return OTA_RESPONSE_OK; @@ -29,7 +34,10 @@ OTAResponseTypes ArduinoESP32OTABackend::begin(size_t image_size) { return OTA_RESPONSE_ERROR_UNKNOWN; } -void ArduinoESP32OTABackend::set_update_md5(const char *md5) { Update.setMD5(md5); } +void ArduinoESP32OTABackend::set_update_md5(const char *md5) { + Update.setMD5(md5); + this->md5_set_ = true; +} OTAResponseTypes ArduinoESP32OTABackend::write(uint8_t *data, size_t len) { size_t written = Update.write(data, len); @@ -44,7 +52,9 @@ OTAResponseTypes ArduinoESP32OTABackend::write(uint8_t *data, size_t len) { } OTAResponseTypes ArduinoESP32OTABackend::end() { - if (Update.end()) { + // Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5 + // This matches the behavior of the old web_server OTA implementation + if (Update.end(!this->md5_set_)) { return OTA_RESPONSE_OK; } diff --git a/esphome/components/ota/ota_backend_arduino_esp32.h b/esphome/components/ota/ota_backend_arduino_esp32.h index ac7fe9f14f..6615cf3dc0 100644 --- a/esphome/components/ota/ota_backend_arduino_esp32.h +++ b/esphome/components/ota/ota_backend_arduino_esp32.h @@ -16,6 +16,9 @@ class ArduinoESP32OTABackend : public OTABackend { OTAResponseTypes end() override; void abort() override; bool supports_compression() override { return false; } + + private: + bool md5_set_{false}; }; } // namespace ota diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.cpp b/esphome/components/ota/ota_backend_arduino_esp8266.cpp index 42edbf5d2b..375c4e7200 100644 --- a/esphome/components/ota/ota_backend_arduino_esp8266.cpp +++ b/esphome/components/ota/ota_backend_arduino_esp8266.cpp @@ -17,6 +17,11 @@ static const char *const TAG = "ota.arduino_esp8266"; std::unique_ptr make_ota_backend() { return make_unique(); } OTAResponseTypes ArduinoESP8266OTABackend::begin(size_t image_size) { + // Handle UPDATE_SIZE_UNKNOWN (0) by calculating available space + if (image_size == 0) { + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + image_size = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; + } bool ret = Update.begin(image_size, U_FLASH); if (ret) { esp8266::preferences_prevent_write(true); @@ -38,7 +43,10 @@ OTAResponseTypes ArduinoESP8266OTABackend::begin(size_t image_size) { return OTA_RESPONSE_ERROR_UNKNOWN; } -void ArduinoESP8266OTABackend::set_update_md5(const char *md5) { Update.setMD5(md5); } +void ArduinoESP8266OTABackend::set_update_md5(const char *md5) { + Update.setMD5(md5); + this->md5_set_ = true; +} OTAResponseTypes ArduinoESP8266OTABackend::write(uint8_t *data, size_t len) { size_t written = Update.write(data, len); @@ -53,13 +61,19 @@ OTAResponseTypes ArduinoESP8266OTABackend::write(uint8_t *data, size_t len) { } OTAResponseTypes ArduinoESP8266OTABackend::end() { - if (Update.end()) { + // Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5 + // This matches the behavior of the old web_server OTA implementation + bool success = Update.end(!this->md5_set_); + + // On ESP8266, Update.end() might return false even with error code 0 + // Check the actual error code to determine success + uint8_t error = Update.getError(); + + if (success || error == UPDATE_ERROR_OK) { return OTA_RESPONSE_OK; } - uint8_t error = Update.getError(); ESP_LOGE(TAG, "End error: %d", error); - return OTA_RESPONSE_ERROR_UPDATE_END; } diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.h b/esphome/components/ota/ota_backend_arduino_esp8266.h index 7f44d7c965..e1b9015cc7 100644 --- a/esphome/components/ota/ota_backend_arduino_esp8266.h +++ b/esphome/components/ota/ota_backend_arduino_esp8266.h @@ -21,6 +21,9 @@ class ArduinoESP8266OTABackend : public OTABackend { #else bool supports_compression() override { return false; } #endif + + private: + bool md5_set_{false}; }; } // namespace ota diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.cpp b/esphome/components/ota/ota_backend_arduino_libretiny.cpp index 6b2cf80684..b4ecad1227 100644 --- a/esphome/components/ota/ota_backend_arduino_libretiny.cpp +++ b/esphome/components/ota/ota_backend_arduino_libretiny.cpp @@ -15,6 +15,11 @@ static const char *const TAG = "ota.arduino_libretiny"; std::unique_ptr make_ota_backend() { return make_unique(); } OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) { + // Handle UPDATE_SIZE_UNKNOWN (0) which is used by web server OTA + // where the exact firmware size is unknown due to multipart encoding + if (image_size == 0) { + image_size = UPDATE_SIZE_UNKNOWN; + } bool ret = Update.begin(image_size, U_FLASH); if (ret) { return OTA_RESPONSE_OK; @@ -29,7 +34,10 @@ OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) { return OTA_RESPONSE_ERROR_UNKNOWN; } -void ArduinoLibreTinyOTABackend::set_update_md5(const char *md5) { Update.setMD5(md5); } +void ArduinoLibreTinyOTABackend::set_update_md5(const char *md5) { + Update.setMD5(md5); + this->md5_set_ = true; +} OTAResponseTypes ArduinoLibreTinyOTABackend::write(uint8_t *data, size_t len) { size_t written = Update.write(data, len); @@ -44,7 +52,9 @@ OTAResponseTypes ArduinoLibreTinyOTABackend::write(uint8_t *data, size_t len) { } OTAResponseTypes ArduinoLibreTinyOTABackend::end() { - if (Update.end()) { + // Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5 + // This matches the behavior of the old web_server OTA implementation + if (Update.end(!this->md5_set_)) { return OTA_RESPONSE_OK; } diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.h b/esphome/components/ota/ota_backend_arduino_libretiny.h index 11deb6e2f2..6d9b7a96d5 100644 --- a/esphome/components/ota/ota_backend_arduino_libretiny.h +++ b/esphome/components/ota/ota_backend_arduino_libretiny.h @@ -15,6 +15,9 @@ class ArduinoLibreTinyOTABackend : public OTABackend { OTAResponseTypes end() override; void abort() override; bool supports_compression() override { return false; } + + private: + bool md5_set_{false}; }; } // namespace ota diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.cpp b/esphome/components/ota/ota_backend_arduino_rp2040.cpp index ffeab2e93f..ee1ba48d50 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.cpp +++ b/esphome/components/ota/ota_backend_arduino_rp2040.cpp @@ -17,6 +17,8 @@ static const char *const TAG = "ota.arduino_rp2040"; std::unique_ptr make_ota_backend() { return make_unique(); } OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) { + // OTA size of 0 is not currently handled, but + // web_server is not supported for RP2040, so this is not an issue. bool ret = Update.begin(image_size, U_FLASH); if (ret) { rp2040::preferences_prevent_write(true); @@ -38,7 +40,10 @@ OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) { return OTA_RESPONSE_ERROR_UNKNOWN; } -void ArduinoRP2040OTABackend::set_update_md5(const char *md5) { Update.setMD5(md5); } +void ArduinoRP2040OTABackend::set_update_md5(const char *md5) { + Update.setMD5(md5); + this->md5_set_ = true; +} OTAResponseTypes ArduinoRP2040OTABackend::write(uint8_t *data, size_t len) { size_t written = Update.write(data, len); @@ -53,7 +58,9 @@ OTAResponseTypes ArduinoRP2040OTABackend::write(uint8_t *data, size_t len) { } OTAResponseTypes ArduinoRP2040OTABackend::end() { - if (Update.end()) { + // Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5 + // This matches the behavior of the old web_server OTA implementation + if (Update.end(!this->md5_set_)) { return OTA_RESPONSE_OK; } diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.h b/esphome/components/ota/ota_backend_arduino_rp2040.h index b189964ab3..b9e10d506c 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.h +++ b/esphome/components/ota/ota_backend_arduino_rp2040.h @@ -17,6 +17,9 @@ class ArduinoRP2040OTABackend : public OTABackend { OTAResponseTypes end() override; void abort() override; bool supports_compression() override { return false; } + + private: + bool md5_set_{false}; }; } // namespace ota diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index cad44a5795..97aae09bd9 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -56,7 +56,10 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size) { return OTA_RESPONSE_OK; } -void IDFOTABackend::set_update_md5(const char *expected_md5) { memcpy(this->expected_bin_md5_, expected_md5, 32); } +void IDFOTABackend::set_update_md5(const char *expected_md5) { + memcpy(this->expected_bin_md5_, expected_md5, 32); + this->md5_set_ = true; +} OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) { esp_err_t err = esp_ota_write(this->update_handle_, data, len); @@ -73,10 +76,12 @@ OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) { } OTAResponseTypes IDFOTABackend::end() { - this->md5_.calculate(); - if (!this->md5_.equals_hex(this->expected_bin_md5_)) { - this->abort(); - return OTA_RESPONSE_ERROR_MD5_MISMATCH; + if (this->md5_set_) { + this->md5_.calculate(); + if (!this->md5_.equals_hex(this->expected_bin_md5_)) { + this->abort(); + return OTA_RESPONSE_ERROR_MD5_MISMATCH; + } } esp_err_t err = esp_ota_end(this->update_handle_); this->update_handle_ = 0; diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h index ed66d9b970..6e93982131 100644 --- a/esphome/components/ota/ota_backend_esp_idf.h +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -24,6 +24,7 @@ class IDFOTABackend : public OTABackend { const esp_partition_t *partition_; md5::MD5Digest md5_{}; char expected_bin_md5_[32]; + bool md5_set_{false}; }; } // namespace ota diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index ca145c732b..6890f60014 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -33,6 +33,7 @@ from esphome.const import ( ) from esphome.core import CORE, coroutine_with_priority import esphome.final_validate as fv +from esphome.types import ConfigType AUTO_LOAD = ["json", "web_server_base"] @@ -47,7 +48,7 @@ WebServer = web_server_ns.class_("WebServer", cg.Component, cg.Controller) sorting_groups = {} -def default_url(config): +def default_url(config: ConfigType) -> ConfigType: config = config.copy() if config[CONF_VERSION] == 1: if CONF_CSS_URL not in config: @@ -67,13 +68,27 @@ def default_url(config): return config -def validate_local(config): +def validate_local(config: ConfigType) -> ConfigType: if CONF_LOCAL in config and config[CONF_VERSION] == 1: raise cv.Invalid("'local' is not supported in version 1") return config -def validate_sorting_groups(config): +def validate_ota_removed(config: ConfigType) -> ConfigType: + # Only raise error if OTA is explicitly enabled (True) + # If it's False or not specified, we can safely ignore it + if config.get(CONF_OTA): + raise cv.Invalid( + f"The '{CONF_OTA}' option has been removed from 'web_server'. " + f"Please use the new OTA platform structure instead:\n\n" + f"ota:\n" + f" - platform: web_server\n\n" + f"See https://esphome.io/components/ota for more information." + ) + return config + + +def validate_sorting_groups(config: ConfigType) -> ConfigType: if CONF_SORTING_GROUPS in config and config[CONF_VERSION] != 3: raise cv.Invalid( f"'{CONF_SORTING_GROUPS}' is only supported in 'web_server' version 3" @@ -84,7 +99,7 @@ def validate_sorting_groups(config): def _validate_no_sorting_component( sorting_component: str, webserver_version: int, - config: dict, + config: ConfigType, path: list[str] | None = None, ) -> None: if path is None: @@ -107,7 +122,7 @@ def _validate_no_sorting_component( ) -def _final_validate_sorting(config): +def _final_validate_sorting(config: ConfigType) -> ConfigType: if (webserver_version := config.get(CONF_VERSION)) != 3: _validate_no_sorting_component( CONF_SORTING_WEIGHT, webserver_version, fv.full_config.get() @@ -170,7 +185,7 @@ CONFIG_SCHEMA = cv.All( web_server_base.WebServerBase ), cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean, - cv.Optional(CONF_OTA, default=True): cv.boolean, + cv.Optional(CONF_OTA, default=False): cv.boolean, cv.Optional(CONF_LOG, default=True): cv.boolean, cv.Optional(CONF_LOCAL): cv.boolean, cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group), @@ -188,6 +203,7 @@ CONFIG_SCHEMA = cv.All( default_url, validate_local, validate_sorting_groups, + validate_ota_removed, ) @@ -271,11 +287,8 @@ async def to_code(config): else: cg.add(var.set_css_url(config[CONF_CSS_URL])) cg.add(var.set_js_url(config[CONF_JS_URL])) - cg.add(var.set_allow_ota(config[CONF_OTA])) - if config[CONF_OTA]: - # Define USE_WEBSERVER_OTA based only on web_server OTA config - # This allows web server OTA to work without loading the OTA component - cg.add_define("USE_WEBSERVER_OTA") + # OTA is now handled by the web_server OTA platform + # The CONF_OTA option is kept only for backwards compatibility validation cg.add(var.set_expose_log(config[CONF_LOG])) if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]: cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS") diff --git a/esphome/components/web_server/ota/__init__.py b/esphome/components/web_server/ota/__init__.py new file mode 100644 index 0000000000..3af14fd453 --- /dev/null +++ b/esphome/components/web_server/ota/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +from esphome.components.esp32 import add_idf_component +from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.core import CORE, coroutine_with_priority + +CODEOWNERS = ["@esphome/core"] +DEPENDENCIES = ["network", "web_server_base"] + +web_server_ns = cg.esphome_ns.namespace("web_server") +WebServerOTAComponent = web_server_ns.class_("WebServerOTAComponent", OTAComponent) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(WebServerOTAComponent), + } + ) + .extend(BASE_OTA_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +@coroutine_with_priority(52.0) +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await ota_to_code(var, config) + await cg.register_component(var, config) + cg.add_define("USE_WEBSERVER_OTA") + if CORE.using_esp_idf: + add_idf_component(name="zorxx/multipart-parser", ref="1.0.1") diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp new file mode 100644 index 0000000000..4f8f6fda17 --- /dev/null +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -0,0 +1,210 @@ +#include "ota_web_server.h" +#ifdef USE_WEBSERVER_OTA + +#include "esphome/components/ota/ota_backend.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +#ifdef USE_ARDUINO +#ifdef USE_ESP8266 +#include +#elif defined(USE_ESP32) || defined(USE_LIBRETINY) +#include +#endif +#endif // USE_ARDUINO + +namespace esphome { +namespace web_server { + +static const char *const TAG = "web_server.ota"; + +class OTARequestHandler : public AsyncWebHandler { + public: + OTARequestHandler(WebServerOTAComponent *parent) : parent_(parent) {} + void handleRequest(AsyncWebServerRequest *request) override; + void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, + bool final) override; + bool canHandle(AsyncWebServerRequest *request) const override { + return request->url() == "/update" && request->method() == HTTP_POST; + } + + // NOLINTNEXTLINE(readability-identifier-naming) + bool isRequestHandlerTrivial() const override { return false; } + + protected: + void report_ota_progress_(AsyncWebServerRequest *request); + void schedule_ota_reboot_(); + void ota_init_(const char *filename); + + uint32_t last_ota_progress_{0}; + uint32_t ota_read_length_{0}; + WebServerOTAComponent *parent_; + bool ota_success_{false}; + + private: + std::unique_ptr ota_backend_{nullptr}; +}; + +void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) { + const uint32_t now = millis(); + if (now - this->last_ota_progress_ > 1000) { + float percentage = 0.0f; + if (request->contentLength() != 0) { + // Note: Using contentLength() for progress calculation is technically wrong as it includes + // multipart headers/boundaries, but it's only off by a small amount and we don't have + // access to the actual firmware size until the upload is complete. This is intentional + // as it still gives the user a reasonable progress indication. + percentage = (this->ota_read_length_ * 100.0f) / request->contentLength(); + ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage); + } else { + ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_); + } +#ifdef USE_OTA_STATE_CALLBACK + // Report progress - use call_deferred since we're in web server task + this->parent_->state_callback_.call_deferred(ota::OTA_IN_PROGRESS, percentage, 0); +#endif + this->last_ota_progress_ = now; + } +} + +void OTARequestHandler::schedule_ota_reboot_() { + ESP_LOGI(TAG, "OTA update successful!"); + this->parent_->set_timeout(100, []() { + ESP_LOGI(TAG, "Performing OTA reboot now"); + App.safe_reboot(); + }); +} + +void OTARequestHandler::ota_init_(const char *filename) { + ESP_LOGI(TAG, "OTA Update Start: %s", filename); + this->ota_read_length_ = 0; + this->ota_success_ = false; +} + +void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, + uint8_t *data, size_t len, bool final) { + ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_OK; + + if (index == 0 && !this->ota_backend_) { + // Initialize OTA on first call + this->ota_init_(filename.c_str()); + +#ifdef USE_OTA_STATE_CALLBACK + // Notify OTA started - use call_deferred since we're in web server task + this->parent_->state_callback_.call_deferred(ota::OTA_STARTED, 0.0f, 0); +#endif + + // Platform-specific pre-initialization +#ifdef USE_ARDUINO +#ifdef USE_ESP8266 + Update.runAsync(true); +#endif +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + if (Update.isRunning()) { + Update.abort(); + } +#endif +#endif // USE_ARDUINO + + this->ota_backend_ = ota::make_ota_backend(); + if (!this->ota_backend_) { + ESP_LOGE(TAG, "Failed to create OTA backend"); +#ifdef USE_OTA_STATE_CALLBACK + this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, + static_cast(ota::OTA_RESPONSE_ERROR_UNKNOWN)); +#endif + return; + } + + // Web server OTA uses multipart uploads where the actual firmware size + // is unknown (contentLength includes multipart overhead) + // Pass 0 to indicate unknown size + error_code = this->ota_backend_->begin(0); + if (error_code != ota::OTA_RESPONSE_OK) { + ESP_LOGE(TAG, "OTA begin failed: %d", error_code); + this->ota_backend_.reset(); +#ifdef USE_OTA_STATE_CALLBACK + this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast(error_code)); +#endif + return; + } + } + + if (!this->ota_backend_) { + return; + } + + // Process data + if (len > 0) { + error_code = this->ota_backend_->write(data, len); + if (error_code != ota::OTA_RESPONSE_OK) { + ESP_LOGE(TAG, "OTA write failed: %d", error_code); + this->ota_backend_->abort(); + this->ota_backend_.reset(); +#ifdef USE_OTA_STATE_CALLBACK + this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast(error_code)); +#endif + return; + } + this->ota_read_length_ += len; + this->report_ota_progress_(request); + } + + // Finalize + if (final) { + ESP_LOGD(TAG, "OTA final chunk: index=%u, len=%u, total_read=%u, contentLength=%u", index, len, + this->ota_read_length_, request->contentLength()); + + // For Arduino framework, the Update library tracks expected size from firmware header + // If we haven't received enough data, calling end() will fail + // This can happen if the upload is interrupted or the client disconnects + error_code = this->ota_backend_->end(); + if (error_code == ota::OTA_RESPONSE_OK) { + this->ota_success_ = true; +#ifdef USE_OTA_STATE_CALLBACK + // Report completion before reboot - use call_deferred since we're in web server task + this->parent_->state_callback_.call_deferred(ota::OTA_COMPLETED, 100.0f, 0); +#endif + this->schedule_ota_reboot_(); + } else { + ESP_LOGE(TAG, "OTA end failed: %d", error_code); +#ifdef USE_OTA_STATE_CALLBACK + this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast(error_code)); +#endif + } + this->ota_backend_.reset(); + } +} + +void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) { + AsyncWebServerResponse *response; + // Use the ota_success_ flag to determine the actual result + const char *msg = this->ota_success_ ? "Update Successful!" : "Update Failed!"; + response = request->beginResponse(200, "text/plain", msg); + response->addHeader("Connection", "close"); + request->send(response); +} + +void WebServerOTAComponent::setup() { + // Get the global web server base instance and register our handler + auto *base = web_server_base::global_web_server_base; + if (base == nullptr) { + ESP_LOGE(TAG, "WebServerBase not found"); + this->mark_failed(); + return; + } + + // AsyncWebServer takes ownership of the handler and will delete it when the server is destroyed + base->add_handler(new OTARequestHandler(this)); // NOLINT +#ifdef USE_OTA_STATE_CALLBACK + // Register with global OTA callback system + ota::register_ota_platform(this); +#endif +} + +void WebServerOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, "Web Server OTA"); } + +} // namespace web_server +} // namespace esphome + +#endif // USE_WEBSERVER_OTA diff --git a/esphome/components/web_server/ota/ota_web_server.h b/esphome/components/web_server/ota/ota_web_server.h new file mode 100644 index 0000000000..a7170c0e34 --- /dev/null +++ b/esphome/components/web_server/ota/ota_web_server.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_WEBSERVER_OTA + +#include "esphome/components/ota/ota_backend.h" +#include "esphome/components/web_server_base/web_server_base.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace web_server { + +class WebServerOTAComponent : public ota::OTAComponent { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + protected: + friend class OTARequestHandler; +}; + +} // namespace web_server +} // namespace esphome + +#endif // USE_WEBSERVER_OTA diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index e0027d0b27..d5ded2a02c 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -273,7 +273,11 @@ std::string WebServer::get_config_json() { return json::build_json([this](JsonObject root) { root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); root["comment"] = App.get_comment(); - root["ota"] = this->allow_ota_; +#ifdef USE_WEBSERVER_OTA + root["ota"] = true; // web_server OTA platform is configured +#else + root["ota"] = false; +#endif root["log"] = this->expose_log_; root["lang"] = "en"; }); @@ -299,10 +303,7 @@ void WebServer::setup() { #endif this->base_->add_handler(this); -#ifdef USE_WEBSERVER_OTA - if (this->allow_ota_) - this->base_->add_ota_handler(); -#endif + // OTA is now handled by the web_server OTA platform // doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly // getting a lot of events diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 991bca6fa7..5f175b6bdd 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -212,11 +212,6 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { * @param include_internal Whether internal components should be displayed. */ void set_include_internal(bool include_internal) { include_internal_ = include_internal; } - /** Set whether or not the webserver should expose the OTA form and handler. - * - * @param allow_ota. - */ - void set_allow_ota(bool allow_ota) { this->allow_ota_ = allow_ota; } /** Set whether or not the webserver should expose the Log. * * @param expose_log. @@ -525,7 +520,6 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #ifdef USE_WEBSERVER_JS_INCLUDE const char *js_include_{nullptr}; #endif - bool allow_ota_{true}; bool expose_log_{true}; #ifdef USE_ESP32 std::deque> to_schedule_; diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index c9b38a2dc4..5db0f1cae9 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -192,11 +192,10 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { stream->print(F("

See ESPHome Web API for " "REST API documentation.

")); - if (this->allow_ota_) { - stream->print( - F("

OTA Update

")); - } +#ifdef USE_WEBSERVER_OTA + stream->print(F("

OTA Update

")); +#endif stream->print(F("

Debug Log

"));
 #ifdef USE_WEBSERVER_JS_INCLUDE
   if (this->js_include_ != nullptr) {
diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py
index c17bab2128..754bf7d433 100644
--- a/esphome/components/web_server_base/__init__.py
+++ b/esphome/components/web_server_base/__init__.py
@@ -30,6 +30,7 @@ CONFIG_SCHEMA = cv.Schema(
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     await cg.register_component(var, config)
+    cg.add(cg.RawExpression(f"{web_server_base_ns}::global_web_server_base = {var}"))
 
     if CORE.using_arduino:
         if CORE.is_esp32:
diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp
index 9ad88e09f4..e1c2bc0b25 100644
--- a/esphome/components/web_server_base/web_server_base.cpp
+++ b/esphome/components/web_server_base/web_server_base.cpp
@@ -4,123 +4,12 @@
 #include "esphome/core/helpers.h"
 #include "esphome/core/log.h"
 
-#ifdef USE_ARDUINO
-#include 
-#if defined(USE_ESP32) || defined(USE_LIBRETINY)
-#include 
-#endif
-#ifdef USE_ESP8266
-#include 
-#endif
-#endif
-
-#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA)
-#include 
-#include 
-#endif
-
 namespace esphome {
 namespace web_server_base {
 
 static const char *const TAG = "web_server_base";
 
-#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA)
-// Minimal OTA backend implementation for web server
-// This allows OTA updates via web server without requiring the OTA component
-// TODO: In the future, this should be refactored into a common ota_base component
-// that both web_server and ota components can depend on, avoiding code duplication
-// while keeping the components independent. This would allow both ESP-IDF and Arduino
-// implementations to share the base OTA functionality without requiring the full OTA component.
-// The IDFWebServerOTABackend class is intentionally designed with the same interface
-// as OTABackend to make it easy to swap to using OTABackend when the ota component
-// is split into ota and ota_base in the future.
-class IDFWebServerOTABackend {
- public:
-  bool begin() {
-    this->partition_ = esp_ota_get_next_update_partition(nullptr);
-    if (this->partition_ == nullptr) {
-      ESP_LOGE(TAG, "No OTA partition available");
-      return false;
-    }
-
-#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15
-    // The following function takes longer than the default timeout of WDT due to flash erase
-#if ESP_IDF_VERSION_MAJOR >= 5
-    esp_task_wdt_config_t wdtc;
-    wdtc.idle_core_mask = 0;
-#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0
-    wdtc.idle_core_mask |= (1 << 0);
-#endif
-#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1
-    wdtc.idle_core_mask |= (1 << 1);
-#endif
-    wdtc.timeout_ms = 15000;
-    wdtc.trigger_panic = false;
-    esp_task_wdt_reconfigure(&wdtc);
-#else
-    esp_task_wdt_init(15, false);
-#endif
-#endif
-
-    esp_err_t err = esp_ota_begin(this->partition_, 0, &this->update_handle_);
-
-#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15
-    // Set the WDT back to the configured timeout
-#if ESP_IDF_VERSION_MAJOR >= 5
-    wdtc.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000;
-    esp_task_wdt_reconfigure(&wdtc);
-#else
-    esp_task_wdt_init(CONFIG_ESP_TASK_WDT_TIMEOUT_S, false);
-#endif
-#endif
-
-    if (err != ESP_OK) {
-      esp_ota_abort(this->update_handle_);
-      this->update_handle_ = 0;
-      ESP_LOGE(TAG, "esp_ota_begin failed: %s", esp_err_to_name(err));
-      return false;
-    }
-    return true;
-  }
-
-  bool write(uint8_t *data, size_t len) {
-    esp_err_t err = esp_ota_write(this->update_handle_, data, len);
-    if (err != ESP_OK) {
-      ESP_LOGE(TAG, "esp_ota_write failed: %s", esp_err_to_name(err));
-      return false;
-    }
-    return true;
-  }
-
-  bool end() {
-    esp_err_t err = esp_ota_end(this->update_handle_);
-    this->update_handle_ = 0;
-    if (err != ESP_OK) {
-      ESP_LOGE(TAG, "esp_ota_end failed: %s", esp_err_to_name(err));
-      return false;
-    }
-
-    err = esp_ota_set_boot_partition(this->partition_);
-    if (err != ESP_OK) {
-      ESP_LOGE(TAG, "esp_ota_set_boot_partition failed: %s", esp_err_to_name(err));
-      return false;
-    }
-
-    return true;
-  }
-
-  void abort() {
-    if (this->update_handle_ != 0) {
-      esp_ota_abort(this->update_handle_);
-      this->update_handle_ = 0;
-    }
-  }
-
- private:
-  esp_ota_handle_t update_handle_{0};
-  const esp_partition_t *partition_{nullptr};
-};
-#endif
+WebServerBase *global_web_server_base = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
 void WebServerBase::add_handler(AsyncWebHandler *handler) {
   // remove all handlers
@@ -134,157 +23,6 @@ void WebServerBase::add_handler(AsyncWebHandler *handler) {
   }
 }
 
-#ifdef USE_WEBSERVER_OTA
-void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
-  const uint32_t now = millis();
-  if (now - this->last_ota_progress_ > 1000) {
-    if (request->contentLength() != 0) {
-      float percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
-      ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
-    } else {
-      ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_);
-    }
-    this->last_ota_progress_ = now;
-  }
-}
-
-void OTARequestHandler::schedule_ota_reboot_() {
-  ESP_LOGI(TAG, "OTA update successful!");
-  this->parent_->set_timeout(100, []() {
-    ESP_LOGI(TAG, "Performing OTA reboot now");
-    App.safe_reboot();
-  });
-}
-
-void OTARequestHandler::ota_init_(const char *filename) {
-  ESP_LOGI(TAG, "OTA Update Start: %s", filename);
-  this->ota_read_length_ = 0;
-}
-
-void report_ota_error() {
-#ifdef USE_ARDUINO
-  StreamString ss;
-  Update.printError(ss);
-  ESP_LOGW(TAG, "OTA Update failed! Error: %s", ss.c_str());
-#endif
-}
-
-void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index,
-                                     uint8_t *data, size_t len, bool final) {
-#ifdef USE_ARDUINO
-  bool success;
-  if (index == 0) {
-    this->ota_init_(filename.c_str());
-#ifdef USE_ESP8266
-    Update.runAsync(true);
-    // NOLINTNEXTLINE(readability-static-accessed-through-instance)
-    success = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
-#endif
-#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_LIBRETINY)
-    if (Update.isRunning()) {
-      Update.abort();
-    }
-    success = Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH);
-#endif
-    if (!success) {
-      report_ota_error();
-      return;
-    }
-  } else if (Update.hasError()) {
-    // don't spam logs with errors if something failed at start
-    return;
-  }
-
-  success = Update.write(data, len) == len;
-  if (!success) {
-    report_ota_error();
-    return;
-  }
-  this->ota_read_length_ += len;
-  this->report_ota_progress_(request);
-
-  if (final) {
-    if (Update.end(true)) {
-      this->schedule_ota_reboot_();
-    } else {
-      report_ota_error();
-    }
-  }
-#endif  // USE_ARDUINO
-
-#ifdef USE_ESP_IDF
-  // ESP-IDF implementation
-  if (index == 0 && !this->ota_backend_) {
-    // Initialize OTA on first call
-    this->ota_init_(filename.c_str());
-    this->ota_success_ = false;
-
-    auto *backend = new IDFWebServerOTABackend();
-    if (!backend->begin()) {
-      ESP_LOGE(TAG, "OTA begin failed");
-      delete backend;
-      return;
-    }
-    this->ota_backend_ = backend;
-  }
-
-  auto *backend = static_cast(this->ota_backend_);
-  if (!backend) {
-    return;
-  }
-
-  // Process data
-  if (len > 0) {
-    if (!backend->write(data, len)) {
-      ESP_LOGE(TAG, "OTA write failed");
-      backend->abort();
-      delete backend;
-      this->ota_backend_ = nullptr;
-      return;
-    }
-    this->ota_read_length_ += len;
-    this->report_ota_progress_(request);
-  }
-
-  // Finalize
-  if (final) {
-    this->ota_success_ = backend->end();
-    if (this->ota_success_) {
-      this->schedule_ota_reboot_();
-    } else {
-      ESP_LOGE(TAG, "OTA end failed");
-    }
-    delete backend;
-    this->ota_backend_ = nullptr;
-  }
-#endif  // USE_ESP_IDF
-}
-
-void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) {
-  AsyncWebServerResponse *response;
-#ifdef USE_ARDUINO
-  if (!Update.hasError()) {
-    response = request->beginResponse(200, "text/plain", "Update Successful!");
-  } else {
-    StreamString ss;
-    ss.print("Update Failed: ");
-    Update.printError(ss);
-    response = request->beginResponse(200, "text/plain", ss);
-  }
-#endif  // USE_ARDUINO
-#ifdef USE_ESP_IDF
-  // Send response based on the OTA result
-  response = request->beginResponse(200, "text/plain", this->ota_success_ ? "Update Successful!" : "Update Failed!");
-#endif  // USE_ESP_IDF
-  response->addHeader("Connection", "close");
-  request->send(response);
-}
-
-void WebServerBase::add_ota_handler() {
-  this->add_handler(new OTARequestHandler(this));  // NOLINT
-}
-#endif
-
 float WebServerBase::get_setup_priority() const {
   // Before WiFi (captive portal)
   return setup_priority::WIFI + 2.0f;
diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h
index 09a41956c9..a475238a37 100644
--- a/esphome/components/web_server_base/web_server_base.h
+++ b/esphome/components/web_server_base/web_server_base.h
@@ -17,6 +17,9 @@
 namespace esphome {
 namespace web_server_base {
 
+class WebServerBase;
+extern WebServerBase *global_web_server_base;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+
 namespace internal {
 
 class MiddlewareHandler : public AsyncWebHandler {
@@ -110,18 +113,10 @@ class WebServerBase : public Component {
 
   void add_handler(AsyncWebHandler *handler);
 
-#ifdef USE_WEBSERVER_OTA
-  void add_ota_handler();
-#endif
-
   void set_port(uint16_t port) { port_ = port; }
   uint16_t get_port() const { return port_; }
 
  protected:
-#ifdef USE_WEBSERVER_OTA
-  friend class OTARequestHandler;
-#endif
-
   int initialized_{0};
   uint16_t port_{80};
   std::shared_ptr server_{nullptr};
@@ -129,37 +124,6 @@ class WebServerBase : public Component {
   internal::Credentials credentials_;
 };
 
-#ifdef USE_WEBSERVER_OTA
-class OTARequestHandler : public AsyncWebHandler {
- public:
-  OTARequestHandler(WebServerBase *parent) : parent_(parent) {}
-  void handleRequest(AsyncWebServerRequest *request) override;
-  void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
-                    bool final) override;
-  bool canHandle(AsyncWebServerRequest *request) const override {
-    return request->url() == "/update" && request->method() == HTTP_POST;
-  }
-
-  // NOLINTNEXTLINE(readability-identifier-naming)
-  bool isRequestHandlerTrivial() const override { return false; }
-
- protected:
-  void report_ota_progress_(AsyncWebServerRequest *request);
-  void schedule_ota_reboot_();
-  void ota_init_(const char *filename);
-
-  uint32_t last_ota_progress_{0};
-  uint32_t ota_read_length_{0};
-  WebServerBase *parent_;
-
- private:
-#ifdef USE_ESP_IDF
-  void *ota_backend_{nullptr};
-  bool ota_success_{false};
-#endif
-};
-#endif  // USE_WEBSERVER_OTA
-
 }  // namespace web_server_base
 }  // namespace esphome
 #endif
diff --git a/esphome/components/web_server_idf/__init__.py b/esphome/components/web_server_idf/__init__.py
index fe1c6f2640..506e1c5c13 100644
--- a/esphome/components/web_server_idf/__init__.py
+++ b/esphome/components/web_server_idf/__init__.py
@@ -1,7 +1,5 @@
-from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
+from esphome.components.esp32 import add_idf_sdkconfig_option
 import esphome.config_validation as cv
-from esphome.const import CONF_OTA, CONF_WEB_SERVER
-from esphome.core import CORE
 
 CODEOWNERS = ["@dentra"]
 
@@ -14,7 +12,3 @@ CONFIG_SCHEMA = cv.All(
 async def to_code(config):
     # Increase the maximum supported size of headers section in HTTP request packet to be processed by the server
     add_idf_sdkconfig_option("CONFIG_HTTPD_MAX_REQ_HDR_LEN", 1024)
-    # Check if web_server component has OTA enabled
-    if CORE.config.get(CONF_WEB_SERVER, {}).get(CONF_OTA, True):
-        # Add multipart parser component for ESP-IDF OTA support
-        add_idf_component(name="zorxx/multipart-parser", ref="1.0.1")
diff --git a/esphome/config.py b/esphome/config.py
index 73cc7657cc..c4aa9aea24 100644
--- a/esphome/config.py
+++ b/esphome/config.py
@@ -67,6 +67,42 @@ ConfigPath = list[str | int]
 path_context = contextvars.ContextVar("Config path")
 
 
+def _process_platform_config(
+    result: Config,
+    component_name: str,
+    platform_name: str,
+    platform_config: ConfigType,
+    path: ConfigPath,
+) -> None:
+    """Process a platform configuration and add necessary validation steps.
+
+    This is shared between LoadValidationStep and AutoLoadValidationStep to avoid duplication.
+    """
+    # Get the platform manifest
+    platform = get_platform(component_name, platform_name)
+    if platform is None:
+        result.add_str_error(
+            f"Platform not found: '{component_name}.{platform_name}'", path
+        )
+        return
+
+    # Add platform to loaded integrations
+    CORE.loaded_integrations.add(platform_name)
+    CORE.loaded_platforms.add(f"{component_name}/{platform_name}")
+
+    # Process platform's AUTO_LOAD
+    for load in platform.auto_load:
+        if load not in result:
+            result.add_validation_step(AutoLoadValidationStep(load))
+
+    # Add validation steps for the platform
+    p_domain = f"{component_name}.{platform_name}"
+    result.add_output_path(path, p_domain)
+    result.add_validation_step(
+        MetadataValidationStep(path, p_domain, platform_config, platform)
+    )
+
+
 def _path_begins_with(path: ConfigPath, other: ConfigPath) -> bool:
     if len(path) < len(other):
         return False
@@ -379,26 +415,11 @@ class LoadValidationStep(ConfigValidationStep):
                     path,
                 )
                 continue
-            # Remove temp output path and construct new one
+            # Remove temp output path
             result.remove_output_path(path, p_domain)
-            p_domain = f"{self.domain}.{p_name}"
-            result.add_output_path(path, p_domain)
-            # Try Load platform
-            platform = get_platform(self.domain, p_name)
-            if platform is None:
-                result.add_str_error(f"Platform not found: '{p_domain}'", path)
-                continue
-            CORE.loaded_integrations.add(p_name)
-            CORE.loaded_platforms.add(f"{self.domain}/{p_name}")
 
-            # Process AUTO_LOAD
-            for load in platform.auto_load:
-                if load not in result:
-                    result.add_validation_step(AutoLoadValidationStep(load))
-
-            result.add_validation_step(
-                MetadataValidationStep(path, p_domain, p_config, platform)
-            )
+            # Process the platform configuration
+            _process_platform_config(result, self.domain, p_name, p_config, path)
 
 
 class AutoLoadValidationStep(ConfigValidationStep):
@@ -413,10 +434,56 @@ class AutoLoadValidationStep(ConfigValidationStep):
         self.domain = domain
 
     def run(self, result: Config) -> None:
-        if self.domain in result:
-            # already loaded
+        # Regular component auto-load (no platform)
+        if "." not in self.domain:
+            if self.domain in result:
+                # already loaded
+                return
+            result.add_validation_step(LoadValidationStep(self.domain, core.AutoLoad()))
             return
-        result.add_validation_step(LoadValidationStep(self.domain, core.AutoLoad()))
+
+        # Platform-specific auto-load (e.g., "ota.web_server")
+        component_name, _, platform_name = self.domain.partition(".")
+
+        # Check if component exists
+        if component_name not in result:
+            # Component doesn't exist, load it first
+            result.add_validation_step(LoadValidationStep(component_name, []))
+            # Re-run this step after the component is loaded
+            result.add_validation_step(AutoLoadValidationStep(self.domain))
+            return
+
+        # Component exists, check if it's a platform component
+        component = get_component(component_name)
+        if component is None or not component.is_platform_component:
+            result.add_str_error(
+                f"Component {component_name} is not a platform component, "
+                f"cannot auto-load platform {platform_name}",
+                [component_name],
+            )
+            return
+
+        # Ensure the component config is a list
+        component_conf = result.get(component_name)
+        if not isinstance(component_conf, list):
+            component_conf = result[component_name] = []
+
+        # Check if platform already exists
+        if any(
+            isinstance(conf, dict) and conf.get(CONF_PLATFORM) == platform_name
+            for conf in component_conf
+        ):
+            return
+
+        # Add and process the platform configuration
+        platform_conf = core.AutoLoad()
+        platform_conf[CONF_PLATFORM] = platform_name
+        component_conf.append(platform_conf)
+
+        path = [component_name, len(component_conf) - 1]
+        _process_platform_config(
+            result, component_name, platform_name, platform_conf, path
+        )
 
 
 class MetadataValidationStep(ConfigValidationStep):
diff --git a/tests/component_tests/ota/test_web_server_ota.py b/tests/component_tests/ota/test_web_server_ota.py
new file mode 100644
index 0000000000..0d8ff6f134
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota.py
@@ -0,0 +1,102 @@
+"""Tests for the web_server OTA platform."""
+
+from collections.abc import Callable
+
+
+def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None:
+    """Test that web_server OTA platform generates correct code."""
+    main_cpp = generate_main("tests/component_tests/ota/test_web_server_ota.yaml")
+
+    # Check that the web server OTA component is included
+    assert "WebServerOTAComponent" in main_cpp
+    assert "web_server::WebServerOTAComponent" in main_cpp
+
+    # Check that global web server base is referenced
+    assert "global_web_server_base" in main_cpp
+
+    # Check component is registered
+    assert "App.register_component(web_server_webserverotacomponent_id)" in main_cpp
+
+
+def test_web_server_ota_with_callbacks(generate_main: Callable[[str], str]) -> None:
+    """Test web_server OTA with state callbacks."""
+    main_cpp = generate_main(
+        "tests/component_tests/ota/test_web_server_ota_callbacks.yaml"
+    )
+
+    # Check that web server OTA component is present
+    assert "WebServerOTAComponent" in main_cpp
+
+    # Check that callbacks are configured
+    # The actual callback code is in the component implementation, not main.cpp
+    # But we can check that logger.log statements are present from the callbacks
+    assert "logger.log" in main_cpp
+    assert "OTA started" in main_cpp
+    assert "OTA completed" in main_cpp
+    assert "OTA error" in main_cpp
+
+
+def test_web_server_ota_idf_multipart(generate_main: Callable[[str], str]) -> None:
+    """Test that ESP-IDF builds include multipart parser dependency."""
+    main_cpp = generate_main("tests/component_tests/ota/test_web_server_ota_idf.yaml")
+
+    # Check that web server OTA component is present
+    assert "WebServerOTAComponent" in main_cpp
+
+    # For ESP-IDF builds, the framework type is esp-idf
+    # The multipart parser dependency is added by web_server_idf
+    assert "web_server::WebServerOTAComponent" in main_cpp
+
+
+def test_web_server_ota_without_web_server_fails(
+    generate_main: Callable[[str], str],
+) -> None:
+    """Test that web_server OTA requires web_server component."""
+    # This should fail during validation since web_server_base is required
+    # but we can't test validation failures with generate_main
+    # Instead, verify that both components are needed in valid config
+    main_cpp = generate_main("tests/component_tests/ota/test_web_server_ota.yaml")
+
+    # Both web server and OTA components should be present
+    assert "WebServer" in main_cpp
+    assert "WebServerOTAComponent" in main_cpp
+
+
+def test_multiple_ota_platforms(generate_main: Callable[[str], str]) -> None:
+    """Test multiple OTA platforms can coexist."""
+    main_cpp = generate_main("tests/component_tests/ota/test_web_server_ota_multi.yaml")
+
+    # Check all OTA platforms are included
+    assert "WebServerOTAComponent" in main_cpp
+    assert "ESPHomeOTAComponent" in main_cpp
+    assert "OtaHttpRequestComponent" in main_cpp
+
+    # Check components are from correct namespaces
+    assert "web_server::WebServerOTAComponent" in main_cpp
+    assert "esphome::ESPHomeOTAComponent" in main_cpp
+    assert "http_request::OtaHttpRequestComponent" in main_cpp
+
+
+def test_web_server_ota_arduino_with_auth(generate_main: Callable[[str], str]) -> None:
+    """Test web_server OTA with Arduino framework and authentication."""
+    main_cpp = generate_main(
+        "tests/component_tests/ota/test_web_server_ota_arduino.yaml"
+    )
+
+    # Check web server OTA component is present
+    assert "WebServerOTAComponent" in main_cpp
+
+    # Check authentication is set up for web server
+    assert "set_auth_username" in main_cpp
+    assert "set_auth_password" in main_cpp
+
+
+def test_web_server_ota_esp8266(generate_main: Callable[[str], str]) -> None:
+    """Test web_server OTA on ESP8266 platform."""
+    main_cpp = generate_main(
+        "tests/component_tests/ota/test_web_server_ota_esp8266.yaml"
+    )
+
+    # Check web server OTA component is present
+    assert "WebServerOTAComponent" in main_cpp
+    assert "web_server::WebServerOTAComponent" in main_cpp
diff --git a/tests/component_tests/ota/test_web_server_ota.yaml b/tests/component_tests/ota/test_web_server_ota.yaml
new file mode 100644
index 0000000000..e0fda3d0b5
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota.yaml
@@ -0,0 +1,15 @@
+esphome:
+  name: test_web_server_ota
+
+esp32:
+  board: esp32dev
+
+wifi:
+  ssid: MySSID
+  password: password1
+
+web_server:
+  port: 80
+
+ota:
+  - platform: web_server
diff --git a/tests/component_tests/ota/test_web_server_ota_arduino.yaml b/tests/component_tests/ota/test_web_server_ota_arduino.yaml
new file mode 100644
index 0000000000..9462548cc8
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota_arduino.yaml
@@ -0,0 +1,18 @@
+esphome:
+  name: test_web_server_ota_arduino
+
+esp32:
+  board: esp32dev
+
+wifi:
+  ssid: MySSID
+  password: password1
+
+web_server:
+  port: 80
+  auth:
+    username: admin
+    password: admin
+
+ota:
+  - platform: web_server
diff --git a/tests/component_tests/ota/test_web_server_ota_callbacks.yaml b/tests/component_tests/ota/test_web_server_ota_callbacks.yaml
new file mode 100644
index 0000000000..c2fd9e0f19
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota_callbacks.yaml
@@ -0,0 +1,31 @@
+esphome:
+  name: test_web_server_ota_callbacks
+
+esp32:
+  board: esp32dev
+
+wifi:
+  ssid: MySSID
+  password: password1
+
+logger:
+
+web_server:
+  port: 80
+
+ota:
+  - platform: web_server
+    on_begin:
+      - logger.log: "OTA started"
+    on_progress:
+      - logger.log:
+          format: "OTA progress: %.1f%%"
+          args: ["x"]
+    on_end:
+      - logger.log: "OTA completed"
+    on_error:
+      - logger.log:
+          format: "OTA error: %d"
+          args: ["x"]
+    on_state_change:
+      - logger.log: "OTA state changed"
diff --git a/tests/component_tests/ota/test_web_server_ota_esp8266.yaml b/tests/component_tests/ota/test_web_server_ota_esp8266.yaml
new file mode 100644
index 0000000000..a1b66a5b53
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota_esp8266.yaml
@@ -0,0 +1,15 @@
+esphome:
+  name: test_web_server_ota_esp8266
+
+esp8266:
+  board: nodemcuv2
+
+wifi:
+  ssid: MySSID
+  password: password1
+
+web_server:
+  port: 80
+
+ota:
+  - platform: web_server
diff --git a/tests/component_tests/ota/test_web_server_ota_idf.yaml b/tests/component_tests/ota/test_web_server_ota_idf.yaml
new file mode 100644
index 0000000000..18b639347c
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota_idf.yaml
@@ -0,0 +1,17 @@
+esphome:
+  name: test_web_server_ota_idf
+
+esp32:
+  board: esp32dev
+  framework:
+    type: esp-idf
+
+wifi:
+  ssid: MySSID
+  password: password1
+
+web_server:
+  port: 80
+
+ota:
+  - platform: web_server
diff --git a/tests/component_tests/ota/test_web_server_ota_multi.yaml b/tests/component_tests/ota/test_web_server_ota_multi.yaml
new file mode 100644
index 0000000000..7926b09c71
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota_multi.yaml
@@ -0,0 +1,21 @@
+esphome:
+  name: test_web_server_ota_multi
+
+esp32:
+  board: esp32dev
+
+wifi:
+  ssid: MySSID
+  password: password1
+
+web_server:
+  port: 80
+
+http_request:
+  verify_ssl: false
+
+ota:
+  - platform: esphome
+    password: "test_password"
+  - platform: web_server
+  - platform: http_request
diff --git a/tests/component_tests/web_server/test_ota_migration.py b/tests/component_tests/web_server/test_ota_migration.py
new file mode 100644
index 0000000000..7f34ec75f6
--- /dev/null
+++ b/tests/component_tests/web_server/test_ota_migration.py
@@ -0,0 +1,38 @@
+"""Tests for web_server OTA migration validation."""
+
+import pytest
+
+from esphome import config_validation as cv
+from esphome.types import ConfigType
+
+
+def test_web_server_ota_true_fails_validation() -> None:
+    """Test that web_server with ota: true fails validation with helpful message."""
+    from esphome.components.web_server import validate_ota_removed
+
+    # Config with ota: true should fail
+    config: ConfigType = {"ota": True}
+
+    with pytest.raises(cv.Invalid) as exc_info:
+        validate_ota_removed(config)
+
+    # Check error message contains migration instructions
+    error_msg = str(exc_info.value)
+    assert "has been removed from 'web_server'" in error_msg
+    assert "platform: web_server" in error_msg
+    assert "ota:" in error_msg
+
+
+def test_web_server_ota_false_passes_validation() -> None:
+    """Test that web_server with ota: false passes validation."""
+    from esphome.components.web_server import validate_ota_removed
+
+    # Config with ota: false should pass
+    config: ConfigType = {"ota": False}
+    result = validate_ota_removed(config)
+    assert result == config
+
+    # Config without ota should also pass
+    config: ConfigType = {}
+    result = validate_ota_removed(config)
+    assert result == config
diff --git a/tests/components/web_server/test_no_ota.esp32-idf.yaml b/tests/components/web_server/test_no_ota.esp32-idf.yaml
index 1f677fb948..4064f518cf 100644
--- a/tests/components/web_server/test_no_ota.esp32-idf.yaml
+++ b/tests/components/web_server/test_no_ota.esp32-idf.yaml
@@ -1,3 +1,11 @@
+esphome:
+  name: test-web-server-no-ota-idf
+
+esp32:
+  board: esp32dev
+  framework:
+    type: esp-idf
+
 packages:
   device_base: !include common.yaml
 
@@ -6,4 +14,3 @@ packages:
 web_server:
   port: 8080
   version: 2
-  ota: false
diff --git a/tests/components/web_server/test_ota.esp32-idf.yaml b/tests/components/web_server/test_ota.esp32-idf.yaml
index 294e7f862e..37838b3d34 100644
--- a/tests/components/web_server/test_ota.esp32-idf.yaml
+++ b/tests/components/web_server/test_ota.esp32-idf.yaml
@@ -1,8 +1,6 @@
-# Test configuration for ESP-IDF web server with OTA enabled
 esphome:
   name: test-web-server-ota-idf
 
-# Force ESP-IDF framework
 esp32:
   board: esp32dev
   framework:
@@ -15,17 +13,17 @@ packages:
 ota:
   - platform: esphome
     password: "test_ota_password"
+  - platform: web_server
 
-# Web server with OTA enabled
+# Web server configuration
 web_server:
   port: 8080
   version: 2
-  ota: true
   include_internal: true
 
 # Enable debug logging for OTA
 logger:
-  level: DEBUG
+  level: VERBOSE
   logs:
     web_server: VERBOSE
     web_server_idf: VERBOSE
diff --git a/tests/components/web_server/test_ota_disabled.esp32-idf.yaml b/tests/components/web_server/test_ota_disabled.esp32-idf.yaml
index c7c7574e3b..b88b845db7 100644
--- a/tests/components/web_server/test_ota_disabled.esp32-idf.yaml
+++ b/tests/components/web_server/test_ota_disabled.esp32-idf.yaml
@@ -1,11 +1,18 @@
+esphome:
+  name: test-ws-ota-disabled-idf
+
+esp32:
+  board: esp32dev
+  framework:
+    type: esp-idf
+
 packages:
   device_base: !include common.yaml
 
-# OTA is configured but web_server OTA is disabled
+# OTA is configured but web_server OTA is NOT included
 ota:
   - platform: esphome
 
 web_server:
   port: 8080
   version: 2
-  ota: false

From 785b14ac84fa0cacc88af0923c36170ea3a07518 Mon Sep 17 00:00:00 2001
From: George 
Date: Wed, 2 Jul 2025 04:14:16 +0200
Subject: [PATCH 203/293] pulse_meter total (#9282)

---
 esphome/components/pulse_meter/pulse_meter_sensor.cpp | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp
index 81ecf22c71..9a7630a7be 100644
--- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp
+++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp
@@ -31,6 +31,10 @@ void PulseMeterSensor::setup() {
     this->pulse_state_.latched_ = this->last_pin_val_;
     this->pin_->attach_interrupt(PulseMeterSensor::pulse_intr, this, gpio::INTERRUPT_ANY_EDGE);
   }
+
+  if (this->total_sensor_ != nullptr) {
+    this->total_sensor_->publish_state(this->total_pulses_);
+  }
 }
 
 void PulseMeterSensor::loop() {

From 5fa9d22c5dd925bd92eaf3297c25b33e8d3abff6 Mon Sep 17 00:00:00 2001
From: Craig Andrews 
Date: Tue, 1 Jul 2025 22:17:34 -0400
Subject: [PATCH 204/293] [http_request] allow retrieval of more than just the
 first header (#9242)

---
 esphome/components/http_request/http_request_arduino.cpp | 1 -
 esphome/components/http_request/http_request_idf.cpp     | 1 -
 2 files changed, 2 deletions(-)

diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp
index b4378cdce6..c009b33c2d 100644
--- a/esphome/components/http_request/http_request_arduino.cpp
+++ b/esphome/components/http_request/http_request_arduino.cpp
@@ -133,7 +133,6 @@ std::shared_ptr HttpRequestArduino::perform(std::string url, std:
       std::string header_value = container->client_.header(i).c_str();
       ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
       container->response_headers_[header_name].push_back(header_value);
-      break;
     }
   }
 
diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp
index 6a779ba03a..68c06d28f2 100644
--- a/esphome/components/http_request/http_request_idf.cpp
+++ b/esphome/components/http_request/http_request_idf.cpp
@@ -42,7 +42,6 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
         const std::string header_value = evt->header_value;
         ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
         user_data->response_headers[header_name].push_back(header_value);
-        break;
       }
       break;
     }

From 095acce3e28ccec918968d5af44af1edd85b4a48 Mon Sep 17 00:00:00 2001
From: Jeremy Brown 
Date: Tue, 1 Jul 2025 22:48:42 -0400
Subject: [PATCH 205/293] Mmc5603 fix for devices that don't retrieve chip_id
 (#8959)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/mmc5603/mmc5603.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/mmc5603/mmc5603.cpp b/esphome/components/mmc5603/mmc5603.cpp
index 86b1b23c15..7f78f9592a 100644
--- a/esphome/components/mmc5603/mmc5603.cpp
+++ b/esphome/components/mmc5603/mmc5603.cpp
@@ -39,7 +39,7 @@ void MMC5603Component::setup() {
     return;
   }
 
-  if (id != MMC56X3_CHIP_ID) {
+  if (id != 0 && id != MMC56X3_CHIP_ID) {  // ID is not reported correctly by all chips, 0 on some chips
     ESP_LOGCONFIG(TAG, "Chip Wrong");
     this->error_code_ = ID_REGISTERS;
     this->mark_failed();

From 2fb23becec8726cbcace94951814debf7f872775 Mon Sep 17 00:00:00 2001
From: JonasB2497 <45214989+JonasB2497@users.noreply.github.com>
Date: Wed, 2 Jul 2025 04:56:48 +0200
Subject: [PATCH 206/293] made qr_code elements optional (#8896)

---
 esphome/components/qr_code/__init__.py | 23 +++++++++++++----------
 1 file changed, 13 insertions(+), 10 deletions(-)

diff --git a/esphome/components/qr_code/__init__.py b/esphome/components/qr_code/__init__.py
index 1c5e0471b0..6ff92b8a7f 100644
--- a/esphome/components/qr_code/__init__.py
+++ b/esphome/components/qr_code/__init__.py
@@ -21,21 +21,24 @@ ECC = {
     "HIGH": qrcodegen_Ecc.qrcodegen_Ecc_HIGH,
 }
 
-CONFIG_SCHEMA = cv.Schema(
-    {
-        cv.Required(CONF_ID): cv.declare_id(QRCode),
-        cv.Required(CONF_VALUE): cv.string,
-        cv.Optional(CONF_ECC, default="LOW"): cv.enum(ECC, upper=True),
-    }
+CONFIG_SCHEMA = cv.ensure_list(
+    cv.Schema(
+        {
+            cv.Required(CONF_ID): cv.declare_id(QRCode),
+            cv.Required(CONF_VALUE): cv.string,
+            cv.Optional(CONF_ECC, default="LOW"): cv.enum(ECC, upper=True),
+        }
+    )
 )
 
 
 async def to_code(config):
     cg.add_library("wjtje/qr-code-generator-library", "^1.7.0")
 
-    var = cg.new_Pvariable(config[CONF_ID])
-    cg.add(var.set_value(config[CONF_VALUE]))
-    cg.add(var.set_ecc(ECC[config[CONF_ECC]]))
-    await cg.register_component(var, config)
+    for entry in config:
+        var = cg.new_Pvariable(entry[CONF_ID])
+        cg.add(var.set_value(entry[CONF_VALUE]))
+        cg.add(var.set_ecc(ECC[entry[CONF_ECC]]))
+        await cg.register_component(var, entry)
 
     cg.add_define("USE_QR_CODE")

From fae96e279c810401cc3e751dca062d30399ac71d Mon Sep 17 00:00:00 2001
From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com>
Date: Wed, 2 Jul 2025 05:25:06 +0200
Subject: [PATCH 207/293] [nextion] memory optimization (#9164)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/nextion/nextion.cpp        | 25 ++++++++-----------
 esphome/components/nextion/nextion.h          | 16 ++++++------
 .../components/nextion/nextion_commands.cpp   |  4 +--
 3 files changed, 20 insertions(+), 25 deletions(-)

diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp
index 042a595ff8..bb75385d8c 100644
--- a/esphome/components/nextion/nextion.cpp
+++ b/esphome/components/nextion/nextion.cpp
@@ -164,7 +164,7 @@ void Nextion::dump_config() {
 #endif  // USE_NEXTION_MAX_COMMANDS_PER_LOOP
 
   if (this->touch_sleep_timeout_ != 0) {
-    ESP_LOGCONFIG(TAG, "  Touch Timeout:  %" PRIu32, this->touch_sleep_timeout_);
+    ESP_LOGCONFIG(TAG, "  Touch Timeout:  %" PRIu16, this->touch_sleep_timeout_);
   }
 
   if (this->wake_up_page_ != -1) {
@@ -302,11 +302,11 @@ void Nextion::loop() {
     }
 
     // Check if a startup page has been set and send the command
-    if (this->start_up_page_ != -1) {
+    if (this->start_up_page_ >= 0) {
       this->goto_page(this->start_up_page_);
     }
 
-    if (this->wake_up_page_ != -1) {
+    if (this->wake_up_page_ >= 0) {
       this->set_wake_up_page(this->wake_up_page_);
     }
 
@@ -418,12 +418,12 @@ void Nextion::process_nextion_commands_() {
       ESP_LOGN(TAG, "Add 0xFF");
     }
 
-    this->nextion_event_ = this->command_data_[0];
+    const uint8_t nextion_event = this->command_data_[0];
 
     to_process_length -= 1;
     to_process = this->command_data_.substr(1, to_process_length);
 
-    switch (this->nextion_event_) {
+    switch (nextion_event) {
       case 0x00:  // instruction sent by user has failed
         ESP_LOGW(TAG, "Invalid instruction");
         this->remove_from_q_();
@@ -562,9 +562,9 @@ void Nextion::process_nextion_commands_() {
           break;
         }
 
-        uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1];
-        uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3];
-        uint8_t touch_event = to_process[4];  // 0 -> release, 1 -> press
+        const uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1];
+        const uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3];
+        const uint8_t touch_event = to_process[4];  // 0 -> release, 1 -> press
         ESP_LOGD(TAG, "Touch %s at %u,%u", touch_event ? "PRESS" : "RELEASE", x, y);
         break;
       }
@@ -820,15 +820,14 @@ void Nextion::process_nextion_commands_() {
         break;
       }
       default:
-        ESP_LOGW(TAG, "Unknown event: 0x%02X", this->nextion_event_);
+        ESP_LOGW(TAG, "Unknown event: 0x%02X", nextion_event);
         break;
     }
 
-    // ESP_LOGN(TAG, "nextion_event_ deleting from 0 to %d", to_process_length + COMMAND_DELIMITER.length() + 1);
     this->command_data_.erase(0, to_process_length + COMMAND_DELIMITER.length() + 1);
   }
 
-  uint32_t ms = App.get_loop_component_start_time();
+  const uint32_t ms = App.get_loop_component_start_time();
 
   if (!this->nextion_queue_.empty() && this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) {
     for (size_t i = 0; i < this->nextion_queue_.size(); i++) {
@@ -960,7 +959,6 @@ void Nextion::update_components_by_prefix(const std::string &prefix) {
 }
 
 uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag) {
-  uint16_t ret = 0;
   uint8_t c = 0;
   uint8_t nr_of_ff_bytes = 0;
   bool exit_flag = false;
@@ -1003,8 +1001,7 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool
   if (ff_flag)
     response = response.substr(0, response.length() - 3);  // Remove last 3 0xFF
 
-  ret = response.length();
-  return ret;
+  return response.length();
 }
 
 /**
diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h
index 0cd559d251..0b77d234f5 100644
--- a/esphome/components/nextion/nextion.h
+++ b/esphome/components/nextion/nextion.h
@@ -1190,11 +1190,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
    * After 30 seconds the display will go to sleep. Note: the display will only wakeup by a restart or by setting up
    * `thup`.
    */
-  void set_touch_sleep_timeout(uint32_t touch_sleep_timeout);
+  void set_touch_sleep_timeout(uint16_t touch_sleep_timeout);
 
   /**
    * Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode.
-   * @param wake_up_page The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to
+   * @param wake_up_page The page id, from 0 to the last page in Nextion. Set -1 (not set to any existing page) to
    * wakes up to current page.
    *
    * Example:
@@ -1204,11 +1204,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
    *
    * The display will wake up to page 2.
    */
-  void set_wake_up_page(uint8_t wake_up_page = 255);
+  void set_wake_up_page(int16_t wake_up_page = -1);
 
   /**
    * Sets which page Nextion loads when connecting to ESPHome.
-   * @param start_up_page The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to
+   * @param start_up_page The page id, from 0 to the last page in Nextion. Set -1 (not set to any existing page) to
    * wakes up to current page.
    *
    * Example:
@@ -1218,7 +1218,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
    *
    * The display will go to page 2 when it establishes a connection to ESPHome.
    */
-  void set_start_up_page(uint8_t start_up_page = 255) { this->start_up_page_ = start_up_page; }
+  void set_start_up_page(int16_t start_up_page = -1) { this->start_up_page_ = start_up_page; }
 
   /**
    * Sets if Nextion should auto-wake from sleep when touch press occurs.
@@ -1330,7 +1330,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
   std::deque waveform_queue_;
   uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag);
   void all_components_send_state_(bool force_update = false);
-  uint64_t comok_sent_ = 0;
+  uint32_t comok_sent_ = 0;
   bool remove_from_q_(bool report_empty = true);
 
   /**
@@ -1340,12 +1340,10 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
   bool ignore_is_setup_ = false;
 
   bool nextion_reports_is_setup_ = false;
-  uint8_t nextion_event_;
-
   void process_nextion_commands_();
   void process_serial_();
   bool is_updating_ = false;
-  uint32_t touch_sleep_timeout_ = 0;
+  uint16_t touch_sleep_timeout_ = 0;
   int16_t wake_up_page_ = -1;
   int16_t start_up_page_ = -1;
   bool auto_wake_on_touch_ = true;
diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp
index 0226e0a13c..84aacd1868 100644
--- a/esphome/components/nextion/nextion_commands.cpp
+++ b/esphome/components/nextion/nextion_commands.cpp
@@ -10,12 +10,12 @@ static const char *const TAG = "nextion";
 // Sleep safe commands
 void Nextion::soft_reset() { this->send_command_("rest"); }
 
-void Nextion::set_wake_up_page(uint8_t wake_up_page) {
+void Nextion::set_wake_up_page(int16_t wake_up_page) {
   this->wake_up_page_ = wake_up_page;
   this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true);
 }
 
-void Nextion::set_touch_sleep_timeout(uint32_t touch_sleep_timeout) {
+void Nextion::set_touch_sleep_timeout(uint16_t touch_sleep_timeout) {
   if (touch_sleep_timeout < 3) {
     ESP_LOGD(TAG, "Sleep timeout out of bounds (3-65535)");
     return;

From eba2c82fec60181327afa35a3be48613d18cc35b Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Tue, 1 Jul 2025 23:36:09 -0500
Subject: [PATCH 208/293] Use encode_bytes() for protobuf bytes fields (#9289)

---
 esphome/components/api/api_pb2.cpp  | 24 +++++++++++++-----------
 script/api_protobuf/api_protobuf.py |  6 +++++-
 2 files changed, 18 insertions(+), 12 deletions(-)

diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp
index 8bce14c9cc..7d16e43ce6 100644
--- a/esphome/components/api/api_pb2.cpp
+++ b/esphome/components/api/api_pb2.cpp
@@ -3494,7 +3494,7 @@ bool SubscribeLogsResponse::decode_length(uint32_t field_id, ProtoLengthDelimite
 }
 void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_enum(1, this->level);
-  buffer.encode_string(3, this->message);
+  buffer.encode_bytes(3, reinterpret_cast(this->message.data()), this->message.size());
   buffer.encode_bool(4, this->send_failed);
 }
 void SubscribeLogsResponse::calculate_size(uint32_t &total_size) const {
@@ -3530,7 +3530,9 @@ bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthD
       return false;
   }
 }
-void NoiseEncryptionSetKeyRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->key); }
+void NoiseEncryptionSetKeyRequest::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_bytes(1, reinterpret_cast(this->key.data()), this->key.size());
+}
 void NoiseEncryptionSetKeyRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->key, false);
 }
@@ -4267,7 +4269,7 @@ bool CameraImageResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
 }
 void CameraImageResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
-  buffer.encode_string(2, this->data);
+  buffer.encode_bytes(2, reinterpret_cast(this->data.data()), this->data.size());
   buffer.encode_bool(3, this->done);
 }
 void CameraImageResponse::calculate_size(uint32_t &total_size) const {
@@ -6785,7 +6787,7 @@ void BluetoothServiceData::encode(ProtoWriteBuffer buffer) const {
   for (auto &it : this->legacy_data) {
     buffer.encode_uint32(2, it, true);
   }
-  buffer.encode_string(3, this->data);
+  buffer.encode_bytes(3, reinterpret_cast(this->data.data()), this->data.size());
 }
 void BluetoothServiceData::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->uuid, false);
@@ -6859,7 +6861,7 @@ bool BluetoothLEAdvertisementResponse::decode_length(uint32_t field_id, ProtoLen
 }
 void BluetoothLEAdvertisementResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_uint64(1, this->address);
-  buffer.encode_string(2, this->name);
+  buffer.encode_bytes(2, reinterpret_cast(this->name.data()), this->name.size());
   buffer.encode_sint32(3, this->rssi);
   for (auto &it : this->service_uuids) {
     buffer.encode_string(4, it, true);
@@ -6960,7 +6962,7 @@ void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_uint64(1, this->address);
   buffer.encode_sint32(2, this->rssi);
   buffer.encode_uint32(3, this->address_type);
-  buffer.encode_string(4, this->data);
+  buffer.encode_bytes(4, reinterpret_cast(this->data.data()), this->data.size());
 }
 void BluetoothLERawAdvertisement::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint64_field(total_size, 1, this->address, false);
@@ -7493,7 +7495,7 @@ bool BluetoothGATTReadResponse::decode_length(uint32_t field_id, ProtoLengthDeli
 void BluetoothGATTReadResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_uint64(1, this->address);
   buffer.encode_uint32(2, this->handle);
-  buffer.encode_string(3, this->data);
+  buffer.encode_bytes(3, reinterpret_cast(this->data.data()), this->data.size());
 }
 void BluetoothGATTReadResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint64_field(total_size, 1, this->address, false);
@@ -7552,7 +7554,7 @@ void BluetoothGATTWriteRequest::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_uint64(1, this->address);
   buffer.encode_uint32(2, this->handle);
   buffer.encode_bool(3, this->response);
-  buffer.encode_string(4, this->data);
+  buffer.encode_bytes(4, reinterpret_cast(this->data.data()), this->data.size());
 }
 void BluetoothGATTWriteRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint64_field(total_size, 1, this->address, false);
@@ -7649,7 +7651,7 @@ bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, Proto
 void BluetoothGATTWriteDescriptorRequest::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_uint64(1, this->address);
   buffer.encode_uint32(2, this->handle);
-  buffer.encode_string(3, this->data);
+  buffer.encode_bytes(3, reinterpret_cast(this->data.data()), this->data.size());
 }
 void BluetoothGATTWriteDescriptorRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint64_field(total_size, 1, this->address, false);
@@ -7751,7 +7753,7 @@ bool BluetoothGATTNotifyDataResponse::decode_length(uint32_t field_id, ProtoLeng
 void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_uint64(1, this->address);
   buffer.encode_uint32(2, this->handle);
-  buffer.encode_string(3, this->data);
+  buffer.encode_bytes(3, reinterpret_cast(this->data.data()), this->data.size());
 }
 void BluetoothGATTNotifyDataResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint64_field(total_size, 1, this->address, false);
@@ -8481,7 +8483,7 @@ bool VoiceAssistantAudio::decode_length(uint32_t field_id, ProtoLengthDelimited
   }
 }
 void VoiceAssistantAudio::encode(ProtoWriteBuffer buffer) const {
-  buffer.encode_string(1, this->data);
+  buffer.encode_bytes(1, reinterpret_cast(this->data.data()), this->data.size());
   buffer.encode_bool(2, this->end);
 }
 void VoiceAssistantAudio::calculate_size(uint32_t &total_size) const {
diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py
index 615f5bbfda..56a46a7701 100755
--- a/script/api_protobuf/api_protobuf.py
+++ b/script/api_protobuf/api_protobuf.py
@@ -526,9 +526,13 @@ class BytesType(TypeInfo):
     reference_type = "std::string &"
     const_reference_type = "const std::string &"
     decode_length = "value.as_string()"
-    encode_func = "encode_string"
+    encode_func = "encode_bytes"
     wire_type = WireType.LENGTH_DELIMITED  # Uses wire type 2
 
+    @property
+    def encode_content(self) -> str:
+        return f"buffer.encode_bytes({self.number}, reinterpret_cast(this->{self.field_name}.data()), this->{self.field_name}.size());"
+
     def dump(self, name: str) -> str:
         o = f"out.append(format_hex_pretty({name}));"
         return o

From f6f0e52d5e7bdef9e2b36990307779d6624a5f36 Mon Sep 17 00:00:00 2001
From: Aleksey Zinchenko 
Date: Wed, 2 Jul 2025 10:37:31 +0300
Subject: [PATCH 209/293] [core] Deleting CMakeCache.txt for fast recompilation
 with ESP-IDF (#8750)

---
 esphome/writer.py | 21 ++++++++++++++++++---
 1 file changed, 18 insertions(+), 3 deletions(-)

diff --git a/esphome/writer.py b/esphome/writer.py
index 7a5089e384..943dfa78cc 100644
--- a/esphome/writer.py
+++ b/esphome/writer.py
@@ -107,6 +107,11 @@ def storage_should_clean(old: StorageJSON, new: StorageJSON) -> bool:
         return True
     if old.build_path != new.build_path:
         return True
+
+    return False
+
+
+def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool:
     if (
         old.loaded_integrations != new.loaded_integrations
         or old.loaded_platforms != new.loaded_platforms
@@ -126,10 +131,11 @@ def update_storage_json():
         return
 
     if storage_should_clean(old, new):
-        _LOGGER.info(
-            "Core config, version or integrations changed, cleaning build files..."
-        )
+        _LOGGER.info("Core config, version changed, cleaning build files...")
         clean_build()
+    elif storage_should_update_cmake_cache(old, new):
+        _LOGGER.info("Integrations changed, cleaning cmake cache...")
+        clean_cmake_cache()
 
     new.save(path)
 
@@ -353,6 +359,15 @@ def write_cpp(code_s):
     write_file_if_changed(path, full_file)
 
 
+def clean_cmake_cache():
+    pioenvs = CORE.relative_pioenvs_path()
+    if os.path.isdir(pioenvs):
+        pioenvs_cmake_path = CORE.relative_pioenvs_path(CORE.name, "CMakeCache.txt")
+        if os.path.isfile(pioenvs_cmake_path):
+            _LOGGER.info("Deleting %s", pioenvs_cmake_path)
+            os.remove(pioenvs_cmake_path)
+
+
 def clean_build():
     import shutil
 

From 56a963dfe68e251acedc25ce598ca4c32bc9783a Mon Sep 17 00:00:00 2001
From: mrtntome <21003287+mrtntome@users.noreply.github.com>
Date: Wed, 2 Jul 2025 09:05:54 -0300
Subject: [PATCH 210/293] [heatpumpir] Add Support for PHS32 HeatPump (#7378)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/heatpumpir/climate.py     | 1 +
 esphome/components/heatpumpir/heatpumpir.cpp | 1 +
 esphome/components/heatpumpir/heatpumpir.h   | 1 +
 3 files changed, 3 insertions(+)

diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py
index 9e5a2bf45c..0f9f146ae9 100644
--- a/esphome/components/heatpumpir/climate.py
+++ b/esphome/components/heatpumpir/climate.py
@@ -70,6 +70,7 @@ PROTOCOLS = {
     "airway": Protocol.PROTOCOL_AIRWAY,
     "bgh_aud": Protocol.PROTOCOL_BGH_AUD,
     "panasonic_altdke": Protocol.PROTOCOL_PANASONIC_ALTDKE,
+    "philco_phs32": Protocol.PROTOCOL_PHILCO_PHS32,
     "vaillantvai8": Protocol.PROTOCOL_VAILLANTVAI8,
     "r51m": Protocol.PROTOCOL_R51M,
 }
diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp
index d3476c6a71..f4d2ca6c1d 100644
--- a/esphome/components/heatpumpir/heatpumpir.cpp
+++ b/esphome/components/heatpumpir/heatpumpir.cpp
@@ -65,6 +65,7 @@ const std::map> PROTOCOL_CONSTRUCTOR_MAP
     {PROTOCOL_AIRWAY, []() { return new AIRWAYHeatpumpIR(); }},                              // NOLINT
     {PROTOCOL_BGH_AUD, []() { return new BGHHeatpumpIR(); }},                                // NOLINT
     {PROTOCOL_PANASONIC_ALTDKE, []() { return new PanasonicAltDKEHeatpumpIR(); }},           // NOLINT
+    {PROTOCOL_PHILCO_PHS32, []() { return new PhilcoPHS32HeatpumpIR(); }},                   // NOLINT
     {PROTOCOL_VAILLANTVAI8, []() { return new VaillantHeatpumpIR(); }},                      // NOLINT
     {PROTOCOL_R51M, []() { return new R51MHeatpumpIR(); }},                                  // NOLINT
 };
diff --git a/esphome/components/heatpumpir/heatpumpir.h b/esphome/components/heatpumpir/heatpumpir.h
index b740d27af7..3e14c11861 100644
--- a/esphome/components/heatpumpir/heatpumpir.h
+++ b/esphome/components/heatpumpir/heatpumpir.h
@@ -65,6 +65,7 @@ enum Protocol {
   PROTOCOL_AIRWAY,
   PROTOCOL_BGH_AUD,
   PROTOCOL_PANASONIC_ALTDKE,
+  PROTOCOL_PHILCO_PHS32,
   PROTOCOL_VAILLANTVAI8,
   PROTOCOL_R51M,
 };

From 4cdc804c178088ee221943accca1398f2c3e1923 Mon Sep 17 00:00:00 2001
From: rwrozelle 
Date: Wed, 2 Jul 2025 08:16:28 -0400
Subject: [PATCH 211/293] OpenThread - add Device Type (#9272)

Co-authored-by: mc 
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/openthread/__init__.py          | 11 ++++++++++-
 esphome/components/openthread/const.py             |  1 +
 tests/components/openthread/test.esp32-c6-idf.yaml |  1 +
 3 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py
index 65138e28c7..25e3153d1b 100644
--- a/esphome/components/openthread/__init__.py
+++ b/esphome/components/openthread/__init__.py
@@ -11,6 +11,7 @@ from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID
 import esphome.final_validate as fv
 
 from .const import (
+    CONF_DEVICE_TYPE,
     CONF_EXT_PAN_ID,
     CONF_FORCE_DATASET,
     CONF_MDNS_ID,
@@ -32,6 +33,11 @@ AUTO_LOAD = ["network"]
 CONFLICTS_WITH = ["wifi"]
 DEPENDENCIES = ["esp32"]
 
+CONF_DEVICE_TYPES = [
+    "FTD",
+    "MTD",
+]
+
 
 def set_sdkconfig_options(config):
     # and expose options for using SPI/UART RCPs
@@ -82,7 +88,7 @@ def set_sdkconfig_options(config):
     add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT_MAX_SERVICES", 5)
 
     # TODO: Add suport for sleepy end devices
-    add_idf_sdkconfig_option("CONFIG_OPENTHREAD_FTD", True)  # Full Thread Device
+    add_idf_sdkconfig_option(f"CONFIG_OPENTHREAD_{config.get(CONF_DEVICE_TYPE)}", True)
 
 
 openthread_ns = cg.esphome_ns.namespace("openthread")
@@ -107,6 +113,9 @@ CONFIG_SCHEMA = cv.All(
             cv.GenerateID(): cv.declare_id(OpenThreadComponent),
             cv.GenerateID(CONF_SRP_ID): cv.declare_id(OpenThreadSrpComponent),
             cv.GenerateID(CONF_MDNS_ID): cv.use_id(MDNSComponent),
+            cv.Optional(CONF_DEVICE_TYPE, default="FTD"): cv.one_of(
+                *CONF_DEVICE_TYPES, upper=True
+            ),
             cv.Optional(CONF_FORCE_DATASET): cv.boolean,
             cv.Optional(CONF_TLV): cv.string_strict,
         }
diff --git a/esphome/components/openthread/const.py b/esphome/components/openthread/const.py
index 7837e69eea..7a6ffb2df4 100644
--- a/esphome/components/openthread/const.py
+++ b/esphome/components/openthread/const.py
@@ -1,3 +1,4 @@
+CONF_DEVICE_TYPE = "device_type"
 CONF_EXT_PAN_ID = "ext_pan_id"
 CONF_FORCE_DATASET = "force_dataset"
 CONF_MDNS_ID = "mdns_id"
diff --git a/tests/components/openthread/test.esp32-c6-idf.yaml b/tests/components/openthread/test.esp32-c6-idf.yaml
index f53b323bec..bbcf48efa5 100644
--- a/tests/components/openthread/test.esp32-c6-idf.yaml
+++ b/tests/components/openthread/test.esp32-c6-idf.yaml
@@ -2,6 +2,7 @@ network:
   enable_ipv6: true
 
 openthread:
+  device_type: FTD
   channel: 13
   network_name: OpenThread-8f28
   network_key: 0xdfd34f0f05cad978ec4e32b0413038ff

From 289aedcfe21c54352ab8da8858d74d5828620287 Mon Sep 17 00:00:00 2001
From: Colm 
Date: Wed, 2 Jul 2025 05:23:37 -0700
Subject: [PATCH 212/293] Don't compile `state_to_string()` unless debugging.
 (#7473)

---
 esphome/components/rtttl/rtttl.cpp | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp
index 2c4a0f917f..65a3af1bbc 100644
--- a/esphome/components/rtttl/rtttl.cpp
+++ b/esphome/components/rtttl/rtttl.cpp
@@ -371,6 +371,7 @@ void Rtttl::finish_() {
   ESP_LOGD(TAG, "Playback finished");
 }
 
+#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
 static const LogString *state_to_string(State state) {
   switch (state) {
     case STATE_STOPPED:
@@ -387,6 +388,7 @@ static const LogString *state_to_string(State state) {
       return LOG_STR("UNKNOWN");
   }
 };
+#endif
 
 void Rtttl::set_state_(State state) {
   State old_state = this->state_;

From 9b3ece4caf0bdaf0633cd91c42966af09d309cce Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Thu, 3 Jul 2025 01:51:25 +1200
Subject: [PATCH 213/293] [time] Add ``USE_TIME_TIMEZONE`` define (#9290)

---
 esphome/components/time/__init__.py         | 18 ++++++++++++++++--
 esphome/components/time/real_time_clock.cpp |  4 ++++
 esphome/components/time/real_time_clock.h   |  4 ++++
 esphome/core/defines.h                      |  1 +
 4 files changed, 25 insertions(+), 2 deletions(-)

diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py
index 6b3ff6f4d3..ab821d457b 100644
--- a/esphome/components/time/__init__.py
+++ b/esphome/components/time/__init__.py
@@ -268,7 +268,19 @@ def validate_tz(value: str) -> str:
 
 TIME_SCHEMA = cv.Schema(
     {
-        cv.Optional(CONF_TIMEZONE, default=detect_tz): validate_tz,
+        cv.SplitDefault(
+            CONF_TIMEZONE,
+            esp8266=detect_tz,
+            esp32=detect_tz,
+            rp2040=detect_tz,
+            bk72xx=detect_tz,
+            rtl87xx=detect_tz,
+            ln882x=detect_tz,
+            host=detect_tz,
+        ): cv.All(
+            cv.only_with_framework(["arduino", "esp-idf", "host"]),
+            validate_tz,
+        ),
         cv.Optional(CONF_ON_TIME): automation.validate_automation(
             {
                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CronTrigger),
@@ -293,7 +305,9 @@ TIME_SCHEMA = cv.Schema(
 
 
 async def setup_time_core_(time_var, config):
-    cg.add(time_var.set_timezone(config[CONF_TIMEZONE]))
+    if timezone := config.get(CONF_TIMEZONE):
+        cg.add(time_var.set_timezone(timezone))
+        cg.add_define("USE_TIME_TIMEZONE")
 
     for conf in config.get(CONF_ON_TIME, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var)
diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp
index 11e39e8f67..61391d2c6b 100644
--- a/esphome/components/time/real_time_clock.cpp
+++ b/esphome/components/time/real_time_clock.cpp
@@ -35,8 +35,10 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
     ret = settimeofday(&timev, nullptr);
   }
 
+#ifdef USE_TIME_TIMEZONE
   // Move timezone back to local timezone.
   this->apply_timezone_();
+#endif
 
   if (ret != 0) {
     ESP_LOGW(TAG, "setimeofday() failed with code %d", ret);
@@ -49,10 +51,12 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
   this->time_sync_callback_.call();
 }
 
+#ifdef USE_TIME_TIMEZONE
 void RealTimeClock::apply_timezone_() {
   setenv("TZ", this->timezone_.c_str(), 1);
   tzset();
 }
+#endif
 
 }  // namespace time
 }  // namespace esphome
diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h
index 401798a568..9fad148885 100644
--- a/esphome/components/time/real_time_clock.h
+++ b/esphome/components/time/real_time_clock.h
@@ -20,6 +20,7 @@ class RealTimeClock : public PollingComponent {
  public:
   explicit RealTimeClock();
 
+#ifdef USE_TIME_TIMEZONE
   /// Set the time zone.
   void set_timezone(const std::string &tz) {
     this->timezone_ = tz;
@@ -28,6 +29,7 @@ class RealTimeClock : public PollingComponent {
 
   /// Get the time zone currently in use.
   std::string get_timezone() { return this->timezone_; }
+#endif
 
   /// Get the time in the currently defined timezone.
   ESPTime now() { return ESPTime::from_epoch_local(this->timestamp_now()); }
@@ -46,8 +48,10 @@ class RealTimeClock : public PollingComponent {
   /// Report a unix epoch as current time.
   void synchronize_epoch_(uint32_t epoch);
 
+#ifdef USE_TIME_TIMEZONE
   std::string timezone_{};
   void apply_timezone_();
+#endif
 
   CallbackManager time_sync_callback_;
 };
diff --git a/esphome/core/defines.h b/esphome/core/defines.h
index cfaed6fdb7..be872689f3 100644
--- a/esphome/core/defines.h
+++ b/esphome/core/defines.h
@@ -116,6 +116,7 @@
 #define USE_OTA_PASSWORD
 #define USE_OTA_STATE_CALLBACK
 #define USE_OTA_VERSION 2
+#define USE_TIME_TIMEZONE
 #define USE_WIFI
 #define USE_WIFI_AP
 #define USE_WIREGUARD

From 60eac6ea0707e0f4dd7d1e506edf1e4c504dbe67 Mon Sep 17 00:00:00 2001
From: tomaszduda23 
Date: Wed, 2 Jul 2025 16:02:56 +0200
Subject: [PATCH 214/293] [time] fix clang-tidy (#9292)

---
 esphome/components/time/real_time_clock.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h
index 9fad148885..4b98a88975 100644
--- a/esphome/components/time/real_time_clock.h
+++ b/esphome/components/time/real_time_clock.h
@@ -40,7 +40,7 @@ class RealTimeClock : public PollingComponent {
   /// Get the current time as the UTC epoch since January 1st 1970.
   time_t timestamp_now() { return ::time(nullptr); }
 
-  void add_on_time_sync_callback(std::function callback) {
+  void add_on_time_sync_callback(std::function &&callback) {
     this->time_sync_callback_.add(std::move(callback));
   };
 

From 00eb56d8db27af8001ba93fbb3552778eab750a2 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Thu, 3 Jul 2025 00:08:10 +1000
Subject: [PATCH 215/293] [esp32_touch] Fix threshold (#9291)

Co-authored-by: Keith Burzinski 
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
 esphome/components/esp32_touch/esp32_touch.h  | 22 ++++----
 .../esp32_touch/esp32_touch_common.cpp        | 17 +++---
 .../components/esp32_touch/esp32_touch_v1.cpp |  6 +-
 .../components/esp32_touch/esp32_touch_v2.cpp | 55 +++++++++----------
 4 files changed, 50 insertions(+), 50 deletions(-)

diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h
index 576c1a5649..5a91b1c750 100644
--- a/esphome/components/esp32_touch/esp32_touch.h
+++ b/esphome/components/esp32_touch/esp32_touch.h
@@ -93,7 +93,6 @@ class ESP32TouchComponent : public Component {
   uint32_t last_release_check_{0};
   uint32_t release_timeout_ms_{1500};
   uint32_t release_check_interval_ms_{50};
-  bool initial_state_published_[TOUCH_PAD_MAX] = {false};
 
   // Common configuration parameters
   uint16_t sleep_cycle_{4095};
@@ -123,13 +122,6 @@ class ESP32TouchComponent : public Component {
   };
 
  protected:
-  // Design note: last_touch_time_ does not require synchronization primitives because:
-  // 1. ESP32 guarantees atomic 32-bit aligned reads/writes
-  // 2. ISR only writes timestamps, main loop only reads
-  // 3. Timing tolerance allows for occasional stale reads (50ms check interval)
-  // 4. Queue operations provide implicit memory barriers
-  // Using atomic/critical sections would add overhead without meaningful benefit
-  uint32_t last_touch_time_[TOUCH_PAD_MAX] = {0};
   uint32_t iir_filter_{0};
 
   bool iir_filter_enabled_() const { return this->iir_filter_ > 0; }
@@ -147,9 +139,6 @@ class ESP32TouchComponent : public Component {
     uint32_t intr_mask;
   };
 
-  // Track last touch time for timeout-based release detection
-  uint32_t last_touch_time_[TOUCH_PAD_MAX] = {0};
-
  protected:
   // Filter configuration
   touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX};
@@ -255,11 +244,22 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor {
 
   touch_pad_t touch_pad_{TOUCH_PAD_MAX};
   uint32_t threshold_{0};
+  uint32_t benchmark_{};
 #ifdef USE_ESP32_VARIANT_ESP32
   uint32_t value_{0};
 #endif
   bool last_state_{false};
   const uint32_t wakeup_threshold_{0};
+
+  // Track last touch time for timeout-based release detection
+  // Design note: last_touch_time_ does not require synchronization primitives because:
+  // 1. ESP32 guarantees atomic 32-bit aligned reads/writes
+  // 2. ISR only writes timestamps, main loop only reads
+  // 3. Timing tolerance allows for occasional stale reads (50ms check interval)
+  // 4. Queue operations provide implicit memory barriers
+  // Using atomic/critical sections would add overhead without meaningful benefit
+  uint32_t last_touch_time_{};
+  bool initial_state_published_{};
 };
 
 }  // namespace esp32_touch
diff --git a/esphome/components/esp32_touch/esp32_touch_common.cpp b/esphome/components/esp32_touch/esp32_touch_common.cpp
index fd2cdfcbad..2d93de077e 100644
--- a/esphome/components/esp32_touch/esp32_touch_common.cpp
+++ b/esphome/components/esp32_touch/esp32_touch_common.cpp
@@ -22,16 +22,20 @@ void ESP32TouchComponent::dump_config_base_() {
                 "  Sleep cycle: %.2fms\n"
                 "  Low Voltage Reference: %s\n"
                 "  High Voltage Reference: %s\n"
-                "  Voltage Attenuation: %s",
+                "  Voltage Attenuation: %s\n"
+                "  Release Timeout: %" PRIu32 "ms\n",
                 this->meas_cycle_ / (8000000.0f / 1000.0f), this->sleep_cycle_ / (150000.0f / 1000.0f), lv_s, hv_s,
-                atten_s);
+                atten_s, this->release_timeout_ms_);
 }
 
 void ESP32TouchComponent::dump_config_sensors_() {
   for (auto *child : this->children_) {
     LOG_BINARY_SENSOR("  ", "Touch Pad", child);
-    ESP_LOGCONFIG(TAG, "    Pad: T%" PRIu32, (uint32_t) child->get_touch_pad());
-    ESP_LOGCONFIG(TAG, "    Threshold: %" PRIu32, child->get_threshold());
+    ESP_LOGCONFIG(TAG,
+                  "    Pad: T%u\n"
+                  "    Threshold: %" PRIu32 "\n"
+                  "    Benchmark: %" PRIu32,
+                  (unsigned) child->touch_pad_, child->threshold_, child->benchmark_);
   }
 }
 
@@ -112,12 +116,11 @@ bool ESP32TouchComponent::should_check_for_releases_(uint32_t now) {
 }
 
 void ESP32TouchComponent::publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now) {
-  touch_pad_t pad = child->get_touch_pad();
-  if (!this->initial_state_published_[pad]) {
+  if (!child->initial_state_published_) {
     // Check if enough time has passed since startup
     if (now > this->release_timeout_ms_) {
       child->publish_initial_state(false);
-      this->initial_state_published_[pad] = true;
+      child->initial_state_published_ = true;
       ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (initial)", child->get_name().c_str());
     }
   }
diff --git a/esphome/components/esp32_touch/esp32_touch_v1.cpp b/esphome/components/esp32_touch/esp32_touch_v1.cpp
index a6d499e9fa..6f05610ed6 100644
--- a/esphome/components/esp32_touch/esp32_touch_v1.cpp
+++ b/esphome/components/esp32_touch/esp32_touch_v1.cpp
@@ -104,7 +104,7 @@ void ESP32TouchComponent::loop() {
 
       // Track when we last saw this pad as touched
       if (new_state) {
-        this->last_touch_time_[event.pad] = now;
+        child->last_touch_time_ = now;
       }
 
       // Only publish if state changed - this filters out repeated events
@@ -127,15 +127,13 @@ void ESP32TouchComponent::loop() {
 
   size_t pads_off = 0;
   for (auto *child : this->children_) {
-    touch_pad_t pad = child->get_touch_pad();
-
     // Handle initial state publication after startup
     this->publish_initial_state_if_needed_(child, now);
 
     if (child->last_state_) {
       // Pad is currently in touched state - check for release timeout
       // Using subtraction handles 32-bit rollover correctly
-      uint32_t time_diff = now - this->last_touch_time_[pad];
+      uint32_t time_diff = now - child->last_touch_time_;
 
       // Check if we haven't seen this pad recently
       if (time_diff > this->release_timeout_ms_) {
diff --git a/esphome/components/esp32_touch/esp32_touch_v2.cpp b/esphome/components/esp32_touch/esp32_touch_v2.cpp
index ad77881724..afd2655fd7 100644
--- a/esphome/components/esp32_touch/esp32_touch_v2.cpp
+++ b/esphome/components/esp32_touch/esp32_touch_v2.cpp
@@ -14,19 +14,16 @@ static const char *const TAG = "esp32_touch";
 void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched) {
   // Always update timer when touched
   if (is_touched) {
-    this->last_touch_time_[child->get_touch_pad()] = App.get_loop_component_start_time();
+    child->last_touch_time_ = App.get_loop_component_start_time();
   }
 
   if (child->last_state_ != is_touched) {
-    // Read value for logging
-    uint32_t value = this->read_touch_value(child->get_touch_pad());
-
     child->last_state_ = is_touched;
     child->publish_state(is_touched);
     if (is_touched) {
       // ESP32-S2/S3 v2: touched when value > threshold
       ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " > threshold: %" PRIu32 ")", child->get_name().c_str(),
-               value, child->get_threshold());
+               this->read_touch_value(child->touch_pad_), child->threshold_ + child->benchmark_);
     } else {
       ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str());
     }
@@ -36,10 +33,13 @@ void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, boo
 // Helper to read touch value and update state for a given child (used for timeout events)
 bool ESP32TouchComponent::check_and_update_touch_state_(ESP32TouchBinarySensor *child) {
   // Read current touch value
-  uint32_t value = this->read_touch_value(child->get_touch_pad());
+  uint32_t value = this->read_touch_value(child->touch_pad_);
 
-  // ESP32-S2/S3 v2: Touch is detected when value > threshold
-  bool is_touched = value > child->get_threshold();
+  // ESP32-S2/S3 v2: Touch is detected when value > threshold + benchmark
+  ESP_LOGV(TAG,
+           "Checking touch state for '%s' (T%d): value = %" PRIu32 ", threshold = %" PRIu32 ", benchmark = %" PRIu32,
+           child->get_name().c_str(), child->touch_pad_, value, child->threshold_, child->benchmark_);
+  bool is_touched = value > child->benchmark_ + child->threshold_;
 
   this->update_touch_state_(child, is_touched);
   return is_touched;
@@ -61,9 +61,9 @@ void ESP32TouchComponent::setup() {
 
   // Configure each touch pad first
   for (auto *child : this->children_) {
-    esp_err_t config_err = touch_pad_config(child->get_touch_pad());
+    esp_err_t config_err = touch_pad_config(child->touch_pad_);
     if (config_err != ESP_OK) {
-      ESP_LOGE(TAG, "Failed to configure touch pad %d: %s", child->get_touch_pad(), esp_err_to_name(config_err));
+      ESP_LOGE(TAG, "Failed to configure touch pad %d: %s", child->touch_pad_, esp_err_to_name(config_err));
     }
   }
 
@@ -100,8 +100,8 @@ void ESP32TouchComponent::setup() {
 
   // Configure measurement parameters
   touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_);
-  // ESP32-S2/S3 always use the older API
-  touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_);
+  touch_pad_set_charge_discharge_times(this->meas_cycle_);
+  touch_pad_set_measurement_interval(this->sleep_cycle_);
 
   // Configure timeout if needed
   touch_pad_timeout_set(true, TOUCH_PAD_THRESHOLD_MAX);
@@ -118,8 +118,8 @@ void ESP32TouchComponent::setup() {
 
   // Set thresholds for each pad BEFORE starting FSM
   for (auto *child : this->children_) {
-    if (child->get_threshold() != 0) {
-      touch_pad_set_thresh(child->get_touch_pad(), child->get_threshold());
+    if (child->threshold_ != 0) {
+      touch_pad_set_thresh(child->touch_pad_, child->threshold_);
     }
   }
 
@@ -277,6 +277,7 @@ void ESP32TouchComponent::loop() {
   // Process any queued touch events from interrupts
   TouchPadEventV2 event;
   while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) {
+    ESP_LOGD(TAG, "Event received, mask = 0x%" PRIx32 ", pad = %d", event.intr_mask, event.pad);
     // Handle timeout events
     if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) {
       // Resume measurement after timeout
@@ -289,18 +290,16 @@ void ESP32TouchComponent::loop() {
 
     // Find the child for the pad that triggered the interrupt
     for (auto *child : this->children_) {
-      if (child->get_touch_pad() != event.pad) {
-        continue;
+      if (child->touch_pad_ == event.pad) {
+        if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) {
+          // For timeout events, we need to read the value to determine state
+          this->check_and_update_touch_state_(child);
+        } else if (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) {
+          // We only get ACTIVE interrupts now, releases are detected by timeout
+          this->update_touch_state_(child, true);  // Always touched for ACTIVE interrupts
+        }
+        break;
       }
-
-      if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) {
-        // For timeout events, we need to read the value to determine state
-        this->check_and_update_touch_state_(child);
-      } else if (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) {
-        // We only get ACTIVE interrupts now, releases are detected by timeout
-        this->update_touch_state_(child, true);  // Always touched for ACTIVE interrupts
-      }
-      break;
     }
   }
 
@@ -311,15 +310,15 @@ void ESP32TouchComponent::loop() {
 
   size_t pads_off = 0;
   for (auto *child : this->children_) {
-    touch_pad_t pad = child->get_touch_pad();
-
+    if (child->benchmark_ == 0)
+      touch_pad_read_benchmark(child->touch_pad_, &child->benchmark_);
     // Handle initial state publication after startup
     this->publish_initial_state_if_needed_(child, now);
 
     if (child->last_state_) {
       // Pad is currently in touched state - check for release timeout
       // Using subtraction handles 32-bit rollover correctly
-      uint32_t time_diff = now - this->last_touch_time_[pad];
+      uint32_t time_diff = now - child->last_touch_time_;
 
       // Check if we haven't seen this pad recently
       if (time_diff > this->release_timeout_ms_) {

From b9391f2cd415172b3ac697fd55ae322094d41006 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mariusz=20Kry=C5=84ski?= 
Date: Wed, 2 Jul 2025 23:15:37 +0200
Subject: [PATCH 216/293] [ds2484] New component (#9147)

---
 CODEOWNERS                                    |   1 +
 esphome/components/ds2484/__init__.py         |   1 +
 esphome/components/ds2484/ds2484.cpp          | 209 ++++++++++++++++++
 esphome/components/ds2484/ds2484.h            |  43 ++++
 esphome/components/ds2484/one_wire.py         |  37 ++++
 tests/components/ds2484/common.yaml           |  11 +
 tests/components/ds2484/test.esp32-ard.yaml   |   5 +
 .../components/ds2484/test.esp32-c3-ard.yaml  |   5 +
 .../components/ds2484/test.esp32-c3-idf.yaml  |   5 +
 tests/components/ds2484/test.esp32-idf.yaml   |   5 +
 tests/components/ds2484/test.esp8266-ard.yaml |   5 +
 tests/components/ds2484/test.rp2040-ard.yaml  |   5 +
 12 files changed, 332 insertions(+)
 create mode 100644 esphome/components/ds2484/__init__.py
 create mode 100644 esphome/components/ds2484/ds2484.cpp
 create mode 100644 esphome/components/ds2484/ds2484.h
 create mode 100644 esphome/components/ds2484/one_wire.py
 create mode 100644 tests/components/ds2484/common.yaml
 create mode 100644 tests/components/ds2484/test.esp32-ard.yaml
 create mode 100644 tests/components/ds2484/test.esp32-c3-ard.yaml
 create mode 100644 tests/components/ds2484/test.esp32-c3-idf.yaml
 create mode 100644 tests/components/ds2484/test.esp32-idf.yaml
 create mode 100644 tests/components/ds2484/test.esp8266-ard.yaml
 create mode 100644 tests/components/ds2484/test.rp2040-ard.yaml

diff --git a/CODEOWNERS b/CODEOWNERS
index 16f38da725..295dd9b1b2 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -124,6 +124,7 @@ esphome/components/dht/* @OttoWinter
 esphome/components/display_menu_base/* @numo68
 esphome/components/dps310/* @kbx81
 esphome/components/ds1307/* @badbadc0ffee
+esphome/components/ds2484/* @mrk-its
 esphome/components/dsmr/* @glmnet @zuidwijk
 esphome/components/duty_time/* @dudanov
 esphome/components/ee895/* @Stock-M
diff --git a/esphome/components/ds2484/__init__.py b/esphome/components/ds2484/__init__.py
new file mode 100644
index 0000000000..3d9f24ff19
--- /dev/null
+++ b/esphome/components/ds2484/__init__.py
@@ -0,0 +1 @@
+CODEOWNERS = ["@mrk-its"]
diff --git a/esphome/components/ds2484/ds2484.cpp b/esphome/components/ds2484/ds2484.cpp
new file mode 100644
index 0000000000..c3df9786b6
--- /dev/null
+++ b/esphome/components/ds2484/ds2484.cpp
@@ -0,0 +1,209 @@
+#include "ds2484.h"
+
+namespace esphome {
+namespace ds2484 {
+static const char *const TAG = "ds2484.onewire";
+
+void DS2484OneWireBus::setup() {
+  ESP_LOGCONFIG(TAG, "Running setup");
+  this->reset_device();
+  this->search();
+}
+
+void DS2484OneWireBus::dump_config() {
+  ESP_LOGCONFIG(TAG, "1-wire bus:");
+  this->dump_devices_(TAG);
+}
+
+bool DS2484OneWireBus::read_status_(uint8_t *status) {
+  for (uint8_t retry_nr = 0; retry_nr < 10; retry_nr++) {
+    if (this->read(status, 1) != i2c::ERROR_OK) {
+      ESP_LOGE(TAG, "read status error");
+      return false;
+    }
+    ESP_LOGVV(TAG, "status: %02x", *status);
+    if (!(*status & 1)) {
+      return true;
+    }
+  }
+  ESP_LOGE(TAG, "read status error: too many retries");
+  return false;
+}
+
+bool DS2484OneWireBus::wait_for_completion_() {
+  uint8_t status;
+  return this->read_status_(&status);
+}
+
+bool DS2484OneWireBus::reset_device() {
+  ESP_LOGVV(TAG, "reset_device");
+  uint8_t device_reset_cmd = 0xf0;
+  uint8_t response;
+  if (this->write(&device_reset_cmd, 1) != i2c::ERROR_OK) {
+    return false;
+  }
+  if (!this->wait_for_completion_()) {
+    ESP_LOGE(TAG, "reset_device: can't complete");
+    return false;
+  }
+  uint8_t config = (this->active_pullup_ ? 1 : 0) | (this->strong_pullup_ ? 4 : 0);
+  uint8_t write_config[2] = {0xd2, (uint8_t) (config | (~config << 4))};
+  if (this->write(write_config, 2) != i2c::ERROR_OK) {
+    ESP_LOGE(TAG, "reset_device: can't write config");
+    return false;
+  }
+  if (this->read(&response, 1) != i2c::ERROR_OK) {
+    ESP_LOGE(TAG, "can't read read8 response");
+    return false;
+  }
+  if (response != (write_config[1] & 0xf)) {
+    ESP_LOGE(TAG, "configuration didn't update");
+    return false;
+  }
+  return true;
+};
+
+int DS2484OneWireBus::reset_int() {
+  ESP_LOGVV(TAG, "reset");
+  uint8_t reset_cmd = 0xb4;
+  if (this->write(&reset_cmd, 1) != i2c::ERROR_OK) {
+    return -1;
+  }
+  return this->wait_for_completion_() ? 1 : 0;
+};
+
+void DS2484OneWireBus::write8_(uint8_t value) {
+  uint8_t buffer[2] = {0xa5, value};
+  this->write(buffer, 2);
+  this->wait_for_completion_();
+};
+
+void DS2484OneWireBus::write8(uint8_t value) {
+  ESP_LOGVV(TAG, "write8: %02x", value);
+  this->write8_(value);
+};
+
+void DS2484OneWireBus::write64(uint64_t value) {
+  ESP_LOGVV(TAG, "write64: %llx", value);
+  for (uint8_t i = 0; i < 8; i++) {
+    this->write8_((value >> (i * 8)) & 0xff);
+  }
+}
+
+uint8_t DS2484OneWireBus::read8() {
+  uint8_t read8_cmd = 0x96;
+  uint8_t set_read_reg_cmd[2] = {0xe1, 0xe1};
+  uint8_t response = 0;
+  if (this->write(&read8_cmd, 1) != i2c::ERROR_OK) {
+    ESP_LOGE(TAG, "can't write read8 cmd");
+    return 0;
+  }
+  this->wait_for_completion_();
+  if (this->write(set_read_reg_cmd, 2) != i2c::ERROR_OK) {
+    ESP_LOGE(TAG, "can't set read data reg");
+    return 0;
+  }
+  if (this->read(&response, 1) != i2c::ERROR_OK) {
+    ESP_LOGE(TAG, "can't read read8 response");
+    return 0;
+  }
+  return response;
+}
+
+uint64_t DS2484OneWireBus::read64() {
+  uint8_t response = 0;
+  for (uint8_t i = 0; i < 8; i++) {
+    response |= (this->read8() << (i * 8));
+  }
+  return response;
+}
+
+void DS2484OneWireBus::reset_search() {
+  this->last_discrepancy_ = 0;
+  this->last_device_flag_ = false;
+  this->address_ = 0;
+}
+
+bool DS2484OneWireBus::one_wire_triple_(bool *branch, bool *id_bit, bool *cmp_id_bit) {
+  uint8_t buffer[2] = {(uint8_t) 0x78, (uint8_t) (*branch ? 0x80u : 0)};
+  uint8_t status;
+  if (!this->read_status_(&status)) {
+    ESP_LOGE(TAG, "one_wire_triple start: read status error");
+    return false;
+  }
+  if (this->write(buffer, 2) != i2c::ERROR_OK) {
+    ESP_LOGV(TAG, "one_wire_triple: can't write cmd");
+    return false;
+  }
+  if (!this->read_status_(&status)) {
+    ESP_LOGE(TAG, "one_wire_triple: read status error");
+    return false;
+  }
+  *id_bit = bool(status & 0x20);
+  *cmp_id_bit = bool(status & 0x40);
+  *branch = bool(status & 0x80);
+  return true;
+}
+
+uint64_t IRAM_ATTR DS2484OneWireBus::search_int() {
+  ESP_LOGVV(TAG, "search_int");
+  if (this->last_device_flag_) {
+    ESP_LOGVV(TAG, "last device flag set, quitting");
+    return 0u;
+  }
+
+  uint8_t last_zero = 0;
+  uint64_t bit_mask = 1;
+  uint64_t address = this->address_;
+
+  // Initiate search
+  for (uint8_t bit_number = 1; bit_number <= 64; bit_number++, bit_mask <<= 1) {
+    bool branch;
+
+    // compute branch value for the case when there is a discrepancy
+    // (there are devices with both 0s and 1s at this bit)
+    if (bit_number < this->last_discrepancy_) {
+      branch = (address & bit_mask) > 0;
+    } else {
+      branch = bit_number == this->last_discrepancy_;
+    }
+
+    bool id_bit, cmp_id_bit;
+    bool branch_before = branch;
+    if (!this->one_wire_triple_(&branch, &id_bit, &cmp_id_bit)) {
+      ESP_LOGW(TAG, "one wire triple error, quitting");
+      return 0;
+    }
+
+    if (id_bit && cmp_id_bit) {
+      ESP_LOGW(TAG, "no devices on the bus, quitting");
+      // No devices participating in search
+      return 0;
+    }
+
+    if (!id_bit && !cmp_id_bit && !branch) {
+      last_zero = bit_number;
+    }
+
+    ESP_LOGVV(TAG, "%d %d branch: %d %d", id_bit, cmp_id_bit, branch_before, branch);
+
+    if (branch) {
+      address |= bit_mask;
+    } else {
+      address &= ~bit_mask;
+    }
+  }
+  ESP_LOGVV(TAG, "last_discepancy: %d", last_zero);
+  ESP_LOGVV(TAG, "address: %llx", address);
+  this->last_discrepancy_ = last_zero;
+  if (this->last_discrepancy_ == 0) {
+    // we're at root and have no choices left, so this was the last one.
+    this->last_device_flag_ = true;
+  }
+
+  this->address_ = address;
+  return address;
+}
+
+}  // namespace ds2484
+}  // namespace esphome
diff --git a/esphome/components/ds2484/ds2484.h b/esphome/components/ds2484/ds2484.h
new file mode 100644
index 0000000000..223227c0a2
--- /dev/null
+++ b/esphome/components/ds2484/ds2484.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/hal.h"
+#include "esphome/core/preferences.h"
+#include "esphome/components/i2c/i2c.h"
+#include "esphome/components/one_wire/one_wire.h"
+
+namespace esphome {
+namespace ds2484 {
+
+class DS2484OneWireBus : public one_wire::OneWireBus, public i2c::I2CDevice, public Component {
+ public:
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override { return setup_priority::BUS - 1.0; }
+
+  bool reset_device();
+  int reset_int() override;
+  void write8(uint8_t) override;
+  void write64(uint64_t) override;
+  uint8_t read8() override;
+  uint64_t read64() override;
+
+  void set_active_pullup(bool value) { this->active_pullup_ = value; }
+  void set_strong_pullup(bool value) { this->strong_pullup_ = value; }
+
+ protected:
+  void reset_search() override;
+  uint64_t search_int() override;
+  bool read_status_(uint8_t *);
+  bool wait_for_completion_();
+  void write8_(uint8_t);
+  bool one_wire_triple_(bool *branch, bool *id_bit, bool *cmp_id_bit);
+
+  uint64_t address_;
+  uint8_t last_discrepancy_{0};
+  bool last_device_flag_{false};
+  bool active_pullup_{false};
+  bool strong_pullup_{false};
+};
+}  // namespace ds2484
+}  // namespace esphome
diff --git a/esphome/components/ds2484/one_wire.py b/esphome/components/ds2484/one_wire.py
new file mode 100644
index 0000000000..384b2d01e6
--- /dev/null
+++ b/esphome/components/ds2484/one_wire.py
@@ -0,0 +1,37 @@
+import esphome.codegen as cg
+from esphome.components import i2c
+from esphome.components.one_wire import OneWireBus
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+ds2484_ns = cg.esphome_ns.namespace("ds2484")
+
+CONF_ACTIVE_PULLUP = "active_pullup"
+CONF_STRONG_PULLUP = "strong_pullup"
+
+CODEOWNERS = ["@mrk-its"]
+DEPENDENCIES = ["i2c"]
+
+DS2484OneWireBus = ds2484_ns.class_(
+    "DS2484OneWireBus", OneWireBus, i2c.I2CDevice, cg.Component
+)
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(DS2484OneWireBus),
+            cv.Optional(CONF_ACTIVE_PULLUP, default=False): cv.boolean,
+            cv.Optional(CONF_STRONG_PULLUP, default=False): cv.boolean,
+        }
+    )
+    .extend(cv.COMPONENT_SCHEMA)
+    .extend(i2c.i2c_device_schema(0x18))
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    cg.add(var.set_active_pullup(config[CONF_ACTIVE_PULLUP]))
+    cg.add(var.set_strong_pullup(config[CONF_STRONG_PULLUP]))
diff --git a/tests/components/ds2484/common.yaml b/tests/components/ds2484/common.yaml
new file mode 100644
index 0000000000..9d2882a3c0
--- /dev/null
+++ b/tests/components/ds2484/common.yaml
@@ -0,0 +1,11 @@
+i2c:
+  - id: i2c_ds2484
+    scl: ${scl_pin}
+    sda: ${sda_pin}
+
+one_wire:
+  platform: ds2484
+  i2c_id: i2c_ds2484
+  address: 0x18
+  active_pullup: true
+  strong_pullup: false
diff --git a/tests/components/ds2484/test.esp32-ard.yaml b/tests/components/ds2484/test.esp32-ard.yaml
new file mode 100644
index 0000000000..63c3bd6afd
--- /dev/null
+++ b/tests/components/ds2484/test.esp32-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO16
+  sda_pin: GPIO17
+
+<<: !include common.yaml
diff --git a/tests/components/ds2484/test.esp32-c3-ard.yaml b/tests/components/ds2484/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/ds2484/test.esp32-c3-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/ds2484/test.esp32-c3-idf.yaml b/tests/components/ds2484/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/ds2484/test.esp32-c3-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/ds2484/test.esp32-idf.yaml b/tests/components/ds2484/test.esp32-idf.yaml
new file mode 100644
index 0000000000..63c3bd6afd
--- /dev/null
+++ b/tests/components/ds2484/test.esp32-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO16
+  sda_pin: GPIO17
+
+<<: !include common.yaml
diff --git a/tests/components/ds2484/test.esp8266-ard.yaml b/tests/components/ds2484/test.esp8266-ard.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/ds2484/test.esp8266-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/ds2484/test.rp2040-ard.yaml b/tests/components/ds2484/test.rp2040-ard.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/ds2484/test.rp2040-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml

From 4ef5c941c935a9e3248203ad3a03730818440cdf Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Wed, 2 Jul 2025 16:39:20 -0500
Subject: [PATCH 217/293] Fix missing ifdef guards in API protobuf generator
 (#9296)

---
 esphome/components/api/api_pb2.cpp       | 4483 +---------------------
 esphome/components/api/api_pb2.h         |   82 +
 esphome/components/api/api_pb2_dump.cpp  | 4228 ++++++++++++++++++++
 esphome/components/api/api_pb2_service.h |    3 +-
 script/api_protobuf/api_protobuf.py      |  359 +-
 5 files changed, 4616 insertions(+), 4539 deletions(-)
 create mode 100644 esphome/components/api/api_pb2_dump.cpp

diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp
index 7d16e43ce6..01140fbfc8 100644
--- a/esphome/components/api/api_pb2.cpp
+++ b/esphome/components/api/api_pb2.cpp
@@ -5,633 +5,9 @@
 #include "esphome/core/log.h"
 #include "esphome/core/helpers.h"
 
-#include 
-
 namespace esphome {
 namespace api {
 
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::EntityCategory value) {
-  switch (value) {
-    case enums::ENTITY_CATEGORY_NONE:
-      return "ENTITY_CATEGORY_NONE";
-    case enums::ENTITY_CATEGORY_CONFIG:
-      return "ENTITY_CATEGORY_CONFIG";
-    case enums::ENTITY_CATEGORY_DIAGNOSTIC:
-      return "ENTITY_CATEGORY_DIAGNOSTIC";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::LegacyCoverState value) {
-  switch (value) {
-    case enums::LEGACY_COVER_STATE_OPEN:
-      return "LEGACY_COVER_STATE_OPEN";
-    case enums::LEGACY_COVER_STATE_CLOSED:
-      return "LEGACY_COVER_STATE_CLOSED";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::CoverOperation value) {
-  switch (value) {
-    case enums::COVER_OPERATION_IDLE:
-      return "COVER_OPERATION_IDLE";
-    case enums::COVER_OPERATION_IS_OPENING:
-      return "COVER_OPERATION_IS_OPENING";
-    case enums::COVER_OPERATION_IS_CLOSING:
-      return "COVER_OPERATION_IS_CLOSING";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::LegacyCoverCommand value) {
-  switch (value) {
-    case enums::LEGACY_COVER_COMMAND_OPEN:
-      return "LEGACY_COVER_COMMAND_OPEN";
-    case enums::LEGACY_COVER_COMMAND_CLOSE:
-      return "LEGACY_COVER_COMMAND_CLOSE";
-    case enums::LEGACY_COVER_COMMAND_STOP:
-      return "LEGACY_COVER_COMMAND_STOP";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::FanSpeed value) {
-  switch (value) {
-    case enums::FAN_SPEED_LOW:
-      return "FAN_SPEED_LOW";
-    case enums::FAN_SPEED_MEDIUM:
-      return "FAN_SPEED_MEDIUM";
-    case enums::FAN_SPEED_HIGH:
-      return "FAN_SPEED_HIGH";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::FanDirection value) {
-  switch (value) {
-    case enums::FAN_DIRECTION_FORWARD:
-      return "FAN_DIRECTION_FORWARD";
-    case enums::FAN_DIRECTION_REVERSE:
-      return "FAN_DIRECTION_REVERSE";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::ColorMode value) {
-  switch (value) {
-    case enums::COLOR_MODE_UNKNOWN:
-      return "COLOR_MODE_UNKNOWN";
-    case enums::COLOR_MODE_ON_OFF:
-      return "COLOR_MODE_ON_OFF";
-    case enums::COLOR_MODE_LEGACY_BRIGHTNESS:
-      return "COLOR_MODE_LEGACY_BRIGHTNESS";
-    case enums::COLOR_MODE_BRIGHTNESS:
-      return "COLOR_MODE_BRIGHTNESS";
-    case enums::COLOR_MODE_WHITE:
-      return "COLOR_MODE_WHITE";
-    case enums::COLOR_MODE_COLOR_TEMPERATURE:
-      return "COLOR_MODE_COLOR_TEMPERATURE";
-    case enums::COLOR_MODE_COLD_WARM_WHITE:
-      return "COLOR_MODE_COLD_WARM_WHITE";
-    case enums::COLOR_MODE_RGB:
-      return "COLOR_MODE_RGB";
-    case enums::COLOR_MODE_RGB_WHITE:
-      return "COLOR_MODE_RGB_WHITE";
-    case enums::COLOR_MODE_RGB_COLOR_TEMPERATURE:
-      return "COLOR_MODE_RGB_COLOR_TEMPERATURE";
-    case enums::COLOR_MODE_RGB_COLD_WARM_WHITE:
-      return "COLOR_MODE_RGB_COLD_WARM_WHITE";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::SensorStateClass value) {
-  switch (value) {
-    case enums::STATE_CLASS_NONE:
-      return "STATE_CLASS_NONE";
-    case enums::STATE_CLASS_MEASUREMENT:
-      return "STATE_CLASS_MEASUREMENT";
-    case enums::STATE_CLASS_TOTAL_INCREASING:
-      return "STATE_CLASS_TOTAL_INCREASING";
-    case enums::STATE_CLASS_TOTAL:
-      return "STATE_CLASS_TOTAL";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::SensorLastResetType value) {
-  switch (value) {
-    case enums::LAST_RESET_NONE:
-      return "LAST_RESET_NONE";
-    case enums::LAST_RESET_NEVER:
-      return "LAST_RESET_NEVER";
-    case enums::LAST_RESET_AUTO:
-      return "LAST_RESET_AUTO";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::LogLevel value) {
-  switch (value) {
-    case enums::LOG_LEVEL_NONE:
-      return "LOG_LEVEL_NONE";
-    case enums::LOG_LEVEL_ERROR:
-      return "LOG_LEVEL_ERROR";
-    case enums::LOG_LEVEL_WARN:
-      return "LOG_LEVEL_WARN";
-    case enums::LOG_LEVEL_INFO:
-      return "LOG_LEVEL_INFO";
-    case enums::LOG_LEVEL_CONFIG:
-      return "LOG_LEVEL_CONFIG";
-    case enums::LOG_LEVEL_DEBUG:
-      return "LOG_LEVEL_DEBUG";
-    case enums::LOG_LEVEL_VERBOSE:
-      return "LOG_LEVEL_VERBOSE";
-    case enums::LOG_LEVEL_VERY_VERBOSE:
-      return "LOG_LEVEL_VERY_VERBOSE";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::ServiceArgType value) {
-  switch (value) {
-    case enums::SERVICE_ARG_TYPE_BOOL:
-      return "SERVICE_ARG_TYPE_BOOL";
-    case enums::SERVICE_ARG_TYPE_INT:
-      return "SERVICE_ARG_TYPE_INT";
-    case enums::SERVICE_ARG_TYPE_FLOAT:
-      return "SERVICE_ARG_TYPE_FLOAT";
-    case enums::SERVICE_ARG_TYPE_STRING:
-      return "SERVICE_ARG_TYPE_STRING";
-    case enums::SERVICE_ARG_TYPE_BOOL_ARRAY:
-      return "SERVICE_ARG_TYPE_BOOL_ARRAY";
-    case enums::SERVICE_ARG_TYPE_INT_ARRAY:
-      return "SERVICE_ARG_TYPE_INT_ARRAY";
-    case enums::SERVICE_ARG_TYPE_FLOAT_ARRAY:
-      return "SERVICE_ARG_TYPE_FLOAT_ARRAY";
-    case enums::SERVICE_ARG_TYPE_STRING_ARRAY:
-      return "SERVICE_ARG_TYPE_STRING_ARRAY";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::ClimateMode value) {
-  switch (value) {
-    case enums::CLIMATE_MODE_OFF:
-      return "CLIMATE_MODE_OFF";
-    case enums::CLIMATE_MODE_HEAT_COOL:
-      return "CLIMATE_MODE_HEAT_COOL";
-    case enums::CLIMATE_MODE_COOL:
-      return "CLIMATE_MODE_COOL";
-    case enums::CLIMATE_MODE_HEAT:
-      return "CLIMATE_MODE_HEAT";
-    case enums::CLIMATE_MODE_FAN_ONLY:
-      return "CLIMATE_MODE_FAN_ONLY";
-    case enums::CLIMATE_MODE_DRY:
-      return "CLIMATE_MODE_DRY";
-    case enums::CLIMATE_MODE_AUTO:
-      return "CLIMATE_MODE_AUTO";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::ClimateFanMode value) {
-  switch (value) {
-    case enums::CLIMATE_FAN_ON:
-      return "CLIMATE_FAN_ON";
-    case enums::CLIMATE_FAN_OFF:
-      return "CLIMATE_FAN_OFF";
-    case enums::CLIMATE_FAN_AUTO:
-      return "CLIMATE_FAN_AUTO";
-    case enums::CLIMATE_FAN_LOW:
-      return "CLIMATE_FAN_LOW";
-    case enums::CLIMATE_FAN_MEDIUM:
-      return "CLIMATE_FAN_MEDIUM";
-    case enums::CLIMATE_FAN_HIGH:
-      return "CLIMATE_FAN_HIGH";
-    case enums::CLIMATE_FAN_MIDDLE:
-      return "CLIMATE_FAN_MIDDLE";
-    case enums::CLIMATE_FAN_FOCUS:
-      return "CLIMATE_FAN_FOCUS";
-    case enums::CLIMATE_FAN_DIFFUSE:
-      return "CLIMATE_FAN_DIFFUSE";
-    case enums::CLIMATE_FAN_QUIET:
-      return "CLIMATE_FAN_QUIET";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::ClimateSwingMode value) {
-  switch (value) {
-    case enums::CLIMATE_SWING_OFF:
-      return "CLIMATE_SWING_OFF";
-    case enums::CLIMATE_SWING_BOTH:
-      return "CLIMATE_SWING_BOTH";
-    case enums::CLIMATE_SWING_VERTICAL:
-      return "CLIMATE_SWING_VERTICAL";
-    case enums::CLIMATE_SWING_HORIZONTAL:
-      return "CLIMATE_SWING_HORIZONTAL";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::ClimateAction value) {
-  switch (value) {
-    case enums::CLIMATE_ACTION_OFF:
-      return "CLIMATE_ACTION_OFF";
-    case enums::CLIMATE_ACTION_COOLING:
-      return "CLIMATE_ACTION_COOLING";
-    case enums::CLIMATE_ACTION_HEATING:
-      return "CLIMATE_ACTION_HEATING";
-    case enums::CLIMATE_ACTION_IDLE:
-      return "CLIMATE_ACTION_IDLE";
-    case enums::CLIMATE_ACTION_DRYING:
-      return "CLIMATE_ACTION_DRYING";
-    case enums::CLIMATE_ACTION_FAN:
-      return "CLIMATE_ACTION_FAN";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::ClimatePreset value) {
-  switch (value) {
-    case enums::CLIMATE_PRESET_NONE:
-      return "CLIMATE_PRESET_NONE";
-    case enums::CLIMATE_PRESET_HOME:
-      return "CLIMATE_PRESET_HOME";
-    case enums::CLIMATE_PRESET_AWAY:
-      return "CLIMATE_PRESET_AWAY";
-    case enums::CLIMATE_PRESET_BOOST:
-      return "CLIMATE_PRESET_BOOST";
-    case enums::CLIMATE_PRESET_COMFORT:
-      return "CLIMATE_PRESET_COMFORT";
-    case enums::CLIMATE_PRESET_ECO:
-      return "CLIMATE_PRESET_ECO";
-    case enums::CLIMATE_PRESET_SLEEP:
-      return "CLIMATE_PRESET_SLEEP";
-    case enums::CLIMATE_PRESET_ACTIVITY:
-      return "CLIMATE_PRESET_ACTIVITY";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::NumberMode value) {
-  switch (value) {
-    case enums::NUMBER_MODE_AUTO:
-      return "NUMBER_MODE_AUTO";
-    case enums::NUMBER_MODE_BOX:
-      return "NUMBER_MODE_BOX";
-    case enums::NUMBER_MODE_SLIDER:
-      return "NUMBER_MODE_SLIDER";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::LockState value) {
-  switch (value) {
-    case enums::LOCK_STATE_NONE:
-      return "LOCK_STATE_NONE";
-    case enums::LOCK_STATE_LOCKED:
-      return "LOCK_STATE_LOCKED";
-    case enums::LOCK_STATE_UNLOCKED:
-      return "LOCK_STATE_UNLOCKED";
-    case enums::LOCK_STATE_JAMMED:
-      return "LOCK_STATE_JAMMED";
-    case enums::LOCK_STATE_LOCKING:
-      return "LOCK_STATE_LOCKING";
-    case enums::LOCK_STATE_UNLOCKING:
-      return "LOCK_STATE_UNLOCKING";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::LockCommand value) {
-  switch (value) {
-    case enums::LOCK_UNLOCK:
-      return "LOCK_UNLOCK";
-    case enums::LOCK_LOCK:
-      return "LOCK_LOCK";
-    case enums::LOCK_OPEN:
-      return "LOCK_OPEN";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::MediaPlayerState value) {
-  switch (value) {
-    case enums::MEDIA_PLAYER_STATE_NONE:
-      return "MEDIA_PLAYER_STATE_NONE";
-    case enums::MEDIA_PLAYER_STATE_IDLE:
-      return "MEDIA_PLAYER_STATE_IDLE";
-    case enums::MEDIA_PLAYER_STATE_PLAYING:
-      return "MEDIA_PLAYER_STATE_PLAYING";
-    case enums::MEDIA_PLAYER_STATE_PAUSED:
-      return "MEDIA_PLAYER_STATE_PAUSED";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::MediaPlayerCommand value) {
-  switch (value) {
-    case enums::MEDIA_PLAYER_COMMAND_PLAY:
-      return "MEDIA_PLAYER_COMMAND_PLAY";
-    case enums::MEDIA_PLAYER_COMMAND_PAUSE:
-      return "MEDIA_PLAYER_COMMAND_PAUSE";
-    case enums::MEDIA_PLAYER_COMMAND_STOP:
-      return "MEDIA_PLAYER_COMMAND_STOP";
-    case enums::MEDIA_PLAYER_COMMAND_MUTE:
-      return "MEDIA_PLAYER_COMMAND_MUTE";
-    case enums::MEDIA_PLAYER_COMMAND_UNMUTE:
-      return "MEDIA_PLAYER_COMMAND_UNMUTE";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::MediaPlayerFormatPurpose value) {
-  switch (value) {
-    case enums::MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT:
-      return "MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT";
-    case enums::MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT:
-      return "MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<>
-const char *proto_enum_to_string(enums::BluetoothDeviceRequestType value) {
-  switch (value) {
-    case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT:
-      return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT";
-    case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT:
-      return "BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT";
-    case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR:
-      return "BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR";
-    case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR:
-      return "BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR";
-    case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE:
-      return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE";
-    case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE:
-      return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE";
-    case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE:
-      return "BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::BluetoothScannerState value) {
-  switch (value) {
-    case enums::BLUETOOTH_SCANNER_STATE_IDLE:
-      return "BLUETOOTH_SCANNER_STATE_IDLE";
-    case enums::BLUETOOTH_SCANNER_STATE_STARTING:
-      return "BLUETOOTH_SCANNER_STATE_STARTING";
-    case enums::BLUETOOTH_SCANNER_STATE_RUNNING:
-      return "BLUETOOTH_SCANNER_STATE_RUNNING";
-    case enums::BLUETOOTH_SCANNER_STATE_FAILED:
-      return "BLUETOOTH_SCANNER_STATE_FAILED";
-    case enums::BLUETOOTH_SCANNER_STATE_STOPPING:
-      return "BLUETOOTH_SCANNER_STATE_STOPPING";
-    case enums::BLUETOOTH_SCANNER_STATE_STOPPED:
-      return "BLUETOOTH_SCANNER_STATE_STOPPED";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::BluetoothScannerMode value) {
-  switch (value) {
-    case enums::BLUETOOTH_SCANNER_MODE_PASSIVE:
-      return "BLUETOOTH_SCANNER_MODE_PASSIVE";
-    case enums::BLUETOOTH_SCANNER_MODE_ACTIVE:
-      return "BLUETOOTH_SCANNER_MODE_ACTIVE";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<>
-const char *proto_enum_to_string(enums::VoiceAssistantSubscribeFlag value) {
-  switch (value) {
-    case enums::VOICE_ASSISTANT_SUBSCRIBE_NONE:
-      return "VOICE_ASSISTANT_SUBSCRIBE_NONE";
-    case enums::VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO:
-      return "VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::VoiceAssistantRequestFlag value) {
-  switch (value) {
-    case enums::VOICE_ASSISTANT_REQUEST_NONE:
-      return "VOICE_ASSISTANT_REQUEST_NONE";
-    case enums::VOICE_ASSISTANT_REQUEST_USE_VAD:
-      return "VOICE_ASSISTANT_REQUEST_USE_VAD";
-    case enums::VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD:
-      return "VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::VoiceAssistantEvent value) {
-  switch (value) {
-    case enums::VOICE_ASSISTANT_ERROR:
-      return "VOICE_ASSISTANT_ERROR";
-    case enums::VOICE_ASSISTANT_RUN_START:
-      return "VOICE_ASSISTANT_RUN_START";
-    case enums::VOICE_ASSISTANT_RUN_END:
-      return "VOICE_ASSISTANT_RUN_END";
-    case enums::VOICE_ASSISTANT_STT_START:
-      return "VOICE_ASSISTANT_STT_START";
-    case enums::VOICE_ASSISTANT_STT_END:
-      return "VOICE_ASSISTANT_STT_END";
-    case enums::VOICE_ASSISTANT_INTENT_START:
-      return "VOICE_ASSISTANT_INTENT_START";
-    case enums::VOICE_ASSISTANT_INTENT_END:
-      return "VOICE_ASSISTANT_INTENT_END";
-    case enums::VOICE_ASSISTANT_TTS_START:
-      return "VOICE_ASSISTANT_TTS_START";
-    case enums::VOICE_ASSISTANT_TTS_END:
-      return "VOICE_ASSISTANT_TTS_END";
-    case enums::VOICE_ASSISTANT_WAKE_WORD_START:
-      return "VOICE_ASSISTANT_WAKE_WORD_START";
-    case enums::VOICE_ASSISTANT_WAKE_WORD_END:
-      return "VOICE_ASSISTANT_WAKE_WORD_END";
-    case enums::VOICE_ASSISTANT_STT_VAD_START:
-      return "VOICE_ASSISTANT_STT_VAD_START";
-    case enums::VOICE_ASSISTANT_STT_VAD_END:
-      return "VOICE_ASSISTANT_STT_VAD_END";
-    case enums::VOICE_ASSISTANT_TTS_STREAM_START:
-      return "VOICE_ASSISTANT_TTS_STREAM_START";
-    case enums::VOICE_ASSISTANT_TTS_STREAM_END:
-      return "VOICE_ASSISTANT_TTS_STREAM_END";
-    case enums::VOICE_ASSISTANT_INTENT_PROGRESS:
-      return "VOICE_ASSISTANT_INTENT_PROGRESS";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::VoiceAssistantTimerEvent value) {
-  switch (value) {
-    case enums::VOICE_ASSISTANT_TIMER_STARTED:
-      return "VOICE_ASSISTANT_TIMER_STARTED";
-    case enums::VOICE_ASSISTANT_TIMER_UPDATED:
-      return "VOICE_ASSISTANT_TIMER_UPDATED";
-    case enums::VOICE_ASSISTANT_TIMER_CANCELLED:
-      return "VOICE_ASSISTANT_TIMER_CANCELLED";
-    case enums::VOICE_ASSISTANT_TIMER_FINISHED:
-      return "VOICE_ASSISTANT_TIMER_FINISHED";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::AlarmControlPanelState value) {
-  switch (value) {
-    case enums::ALARM_STATE_DISARMED:
-      return "ALARM_STATE_DISARMED";
-    case enums::ALARM_STATE_ARMED_HOME:
-      return "ALARM_STATE_ARMED_HOME";
-    case enums::ALARM_STATE_ARMED_AWAY:
-      return "ALARM_STATE_ARMED_AWAY";
-    case enums::ALARM_STATE_ARMED_NIGHT:
-      return "ALARM_STATE_ARMED_NIGHT";
-    case enums::ALARM_STATE_ARMED_VACATION:
-      return "ALARM_STATE_ARMED_VACATION";
-    case enums::ALARM_STATE_ARMED_CUSTOM_BYPASS:
-      return "ALARM_STATE_ARMED_CUSTOM_BYPASS";
-    case enums::ALARM_STATE_PENDING:
-      return "ALARM_STATE_PENDING";
-    case enums::ALARM_STATE_ARMING:
-      return "ALARM_STATE_ARMING";
-    case enums::ALARM_STATE_DISARMING:
-      return "ALARM_STATE_DISARMING";
-    case enums::ALARM_STATE_TRIGGERED:
-      return "ALARM_STATE_TRIGGERED";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<>
-const char *proto_enum_to_string(enums::AlarmControlPanelStateCommand value) {
-  switch (value) {
-    case enums::ALARM_CONTROL_PANEL_DISARM:
-      return "ALARM_CONTROL_PANEL_DISARM";
-    case enums::ALARM_CONTROL_PANEL_ARM_AWAY:
-      return "ALARM_CONTROL_PANEL_ARM_AWAY";
-    case enums::ALARM_CONTROL_PANEL_ARM_HOME:
-      return "ALARM_CONTROL_PANEL_ARM_HOME";
-    case enums::ALARM_CONTROL_PANEL_ARM_NIGHT:
-      return "ALARM_CONTROL_PANEL_ARM_NIGHT";
-    case enums::ALARM_CONTROL_PANEL_ARM_VACATION:
-      return "ALARM_CONTROL_PANEL_ARM_VACATION";
-    case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS:
-      return "ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS";
-    case enums::ALARM_CONTROL_PANEL_TRIGGER:
-      return "ALARM_CONTROL_PANEL_TRIGGER";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::TextMode value) {
-  switch (value) {
-    case enums::TEXT_MODE_TEXT:
-      return "TEXT_MODE_TEXT";
-    case enums::TEXT_MODE_PASSWORD:
-      return "TEXT_MODE_PASSWORD";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::ValveOperation value) {
-  switch (value) {
-    case enums::VALVE_OPERATION_IDLE:
-      return "VALVE_OPERATION_IDLE";
-    case enums::VALVE_OPERATION_IS_OPENING:
-      return "VALVE_OPERATION_IS_OPENING";
-    case enums::VALVE_OPERATION_IS_CLOSING:
-      return "VALVE_OPERATION_IS_CLOSING";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-template<> const char *proto_enum_to_string(enums::UpdateCommand value) {
-  switch (value) {
-    case enums::UPDATE_COMMAND_NONE:
-      return "UPDATE_COMMAND_NONE";
-    case enums::UPDATE_COMMAND_UPDATE:
-      return "UPDATE_COMMAND_UPDATE";
-    case enums::UPDATE_COMMAND_CHECK:
-      return "UPDATE_COMMAND_CHECK";
-    default:
-      return "UNKNOWN";
-  }
-}
-#endif
-
 bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -666,26 +42,6 @@ void HelloRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint32_field(total_size, 1, this->api_version_major, false);
   ProtoSize::add_uint32_field(total_size, 1, this->api_version_minor, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void HelloRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("HelloRequest {\n");
-  out.append("  client_info: ");
-  out.append("'").append(this->client_info).append("'");
-  out.append("\n");
-
-  out.append("  api_version_major: ");
-  sprintf(buffer, "%" PRIu32, this->api_version_major);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  api_version_minor: ");
-  sprintf(buffer, "%" PRIu32, this->api_version_minor);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool HelloResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -726,30 +82,6 @@ void HelloResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->server_info, false);
   ProtoSize::add_string_field(total_size, 1, this->name, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void HelloResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("HelloResponse {\n");
-  out.append("  api_version_major: ");
-  sprintf(buffer, "%" PRIu32, this->api_version_major);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  api_version_minor: ");
-  sprintf(buffer, "%" PRIu32, this->api_version_minor);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  server_info: ");
-  out.append("'").append(this->server_info).append("'");
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
   switch (field_id) {
     case 1: {
@@ -764,16 +96,6 @@ void ConnectRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_strin
 void ConnectRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->password, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ConnectRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ConnectRequest {\n");
-  out.append("  password: ");
-  out.append("'").append(this->password).append("'");
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool ConnectResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -788,31 +110,6 @@ void ConnectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool
 void ConnectResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_bool_field(total_size, 1, this->invalid_password, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ConnectResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ConnectResponse {\n");
-  out.append("  invalid_password: ");
-  out.append(YESNO(this->invalid_password));
-  out.append("\n");
-  out.append("}");
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); }
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); }
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); }
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {}"); }
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); }
-#endif
 bool AreaInfo::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -841,21 +138,6 @@ void AreaInfo::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint32_field(total_size, 1, this->area_id, false);
   ProtoSize::add_string_field(total_size, 1, this->name, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void AreaInfo::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("AreaInfo {\n");
-  out.append("  area_id: ");
-  sprintf(buffer, "%" PRIu32, this->area_id);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool DeviceInfo::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -890,26 +172,6 @@ void DeviceInfo::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->name, false);
   ProtoSize::add_uint32_field(total_size, 1, this->area_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void DeviceInfo::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("DeviceInfo {\n");
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  area_id: ");
-  sprintf(buffer, "%" PRIu32, this->area_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -1062,118 +324,7 @@ void DeviceInfoResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_repeated_message(total_size, 2, this->areas);
   ProtoSize::add_message_object(total_size, 2, this->area, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void DeviceInfoResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("DeviceInfoResponse {\n");
-  out.append("  uses_password: ");
-  out.append(YESNO(this->uses_password));
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  mac_address: ");
-  out.append("'").append(this->mac_address).append("'");
-  out.append("\n");
-
-  out.append("  esphome_version: ");
-  out.append("'").append(this->esphome_version).append("'");
-  out.append("\n");
-
-  out.append("  compilation_time: ");
-  out.append("'").append(this->compilation_time).append("'");
-  out.append("\n");
-
-  out.append("  model: ");
-  out.append("'").append(this->model).append("'");
-  out.append("\n");
-
-  out.append("  has_deep_sleep: ");
-  out.append(YESNO(this->has_deep_sleep));
-  out.append("\n");
-
-  out.append("  project_name: ");
-  out.append("'").append(this->project_name).append("'");
-  out.append("\n");
-
-  out.append("  project_version: ");
-  out.append("'").append(this->project_version).append("'");
-  out.append("\n");
-
-  out.append("  webserver_port: ");
-  sprintf(buffer, "%" PRIu32, this->webserver_port);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  legacy_bluetooth_proxy_version: ");
-  sprintf(buffer, "%" PRIu32, this->legacy_bluetooth_proxy_version);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  bluetooth_proxy_feature_flags: ");
-  sprintf(buffer, "%" PRIu32, this->bluetooth_proxy_feature_flags);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  manufacturer: ");
-  out.append("'").append(this->manufacturer).append("'");
-  out.append("\n");
-
-  out.append("  friendly_name: ");
-  out.append("'").append(this->friendly_name).append("'");
-  out.append("\n");
-
-  out.append("  legacy_voice_assistant_version: ");
-  sprintf(buffer, "%" PRIu32, this->legacy_voice_assistant_version);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  voice_assistant_feature_flags: ");
-  sprintf(buffer, "%" PRIu32, this->voice_assistant_feature_flags);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  suggested_area: ");
-  out.append("'").append(this->suggested_area).append("'");
-  out.append("\n");
-
-  out.append("  bluetooth_mac_address: ");
-  out.append("'").append(this->bluetooth_mac_address).append("'");
-  out.append("\n");
-
-  out.append("  api_encryption_supported: ");
-  out.append(YESNO(this->api_encryption_supported));
-  out.append("\n");
-
-  for (const auto &it : this->devices) {
-    out.append("  devices: ");
-    it.dump_to(out);
-    out.append("\n");
-  }
-
-  for (const auto &it : this->areas) {
-    out.append("  areas: ");
-    it.dump_to(out);
-    out.append("\n");
-  }
-
-  out.append("  area: ");
-  this->area.dump_to(out);
-  out.append("\n");
-  out.append("}");
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesRequest::dump_to(std::string &out) const { out.append("ListEntitiesRequest {}"); }
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesDoneResponse::dump_to(std::string &out) const { out.append("ListEntitiesDoneResponse {}"); }
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void SubscribeStatesRequest::dump_to(std::string &out) const { out.append("SubscribeStatesRequest {}"); }
-#endif
+#ifdef USE_BINARY_SENSOR
 bool ListEntitiesBinarySensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 6: {
@@ -1256,54 +407,6 @@ void ListEntitiesBinarySensorResponse::calculate_size(uint32_t &total_size) cons
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesBinarySensorResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  device_class: ");
-  out.append("'").append(this->device_class).append("'");
-  out.append("\n");
-
-  out.append("  is_status_binary_sensor: ");
-  out.append(YESNO(this->is_status_binary_sensor));
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BinarySensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -1338,25 +441,8 @@ void BinarySensorStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_bool_field(total_size, 1, this->state, false);
   ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BinarySensorStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BinarySensorStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  state: ");
-  out.append(YESNO(this->state));
-  out.append("\n");
-
-  out.append("  missing_state: ");
-  out.append(YESNO(this->missing_state));
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_COVER
 bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 5: {
@@ -1457,66 +543,6 @@ void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_bool_field(total_size, 1, this->supports_stop, false);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesCoverResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesCoverResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  assumed_state: ");
-  out.append(YESNO(this->assumed_state));
-  out.append("\n");
-
-  out.append("  supports_position: ");
-  out.append(YESNO(this->supports_position));
-  out.append("\n");
-
-  out.append("  supports_tilt: ");
-  out.append(YESNO(this->supports_tilt));
-  out.append("\n");
-
-  out.append("  device_class: ");
-  out.append("'").append(this->device_class).append("'");
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  supports_stop: ");
-  out.append(YESNO(this->supports_stop));
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool CoverStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -1563,35 +589,6 @@ void CoverStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->tilt != 0.0f, false);
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->current_operation), false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void CoverStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("CoverStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  legacy_state: ");
-  out.append(proto_enum_to_string(this->legacy_state));
-  out.append("\n");
-
-  out.append("  position: ");
-  sprintf(buffer, "%g", this->position);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  tilt: ");
-  sprintf(buffer, "%g", this->tilt);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  current_operation: ");
-  out.append(proto_enum_to_string(this->current_operation));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -1656,47 +653,8 @@ void CoverCommandRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->tilt != 0.0f, false);
   ProtoSize::add_bool_field(total_size, 1, this->stop, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void CoverCommandRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("CoverCommandRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_legacy_command: ");
-  out.append(YESNO(this->has_legacy_command));
-  out.append("\n");
-
-  out.append("  legacy_command: ");
-  out.append(proto_enum_to_string(this->legacy_command));
-  out.append("\n");
-
-  out.append("  has_position: ");
-  out.append(YESNO(this->has_position));
-  out.append("\n");
-
-  out.append("  position: ");
-  sprintf(buffer, "%g", this->position);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_tilt: ");
-  out.append(YESNO(this->has_tilt));
-  out.append("\n");
-
-  out.append("  tilt: ");
-  sprintf(buffer, "%g", this->tilt);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  stop: ");
-  out.append(YESNO(this->stop));
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_FAN
 bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 5: {
@@ -1803,69 +761,6 @@ void ListEntitiesFanResponse::calculate_size(uint32_t &total_size) const {
   }
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesFanResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesFanResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  supports_oscillation: ");
-  out.append(YESNO(this->supports_oscillation));
-  out.append("\n");
-
-  out.append("  supports_speed: ");
-  out.append(YESNO(this->supports_speed));
-  out.append("\n");
-
-  out.append("  supports_direction: ");
-  out.append(YESNO(this->supports_direction));
-  out.append("\n");
-
-  out.append("  supported_speed_count: ");
-  sprintf(buffer, "%" PRId32, this->supported_speed_count);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  for (const auto &it : this->supported_preset_modes) {
-    out.append("  supported_preset_modes: ");
-    out.append("'").append(it).append("'");
-    out.append("\n");
-  }
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -1930,42 +825,6 @@ void FanStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_int32_field(total_size, 1, this->speed_level, false);
   ProtoSize::add_string_field(total_size, 1, this->preset_mode, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void FanStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("FanStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  state: ");
-  out.append(YESNO(this->state));
-  out.append("\n");
-
-  out.append("  oscillating: ");
-  out.append(YESNO(this->oscillating));
-  out.append("\n");
-
-  out.append("  speed: ");
-  out.append(proto_enum_to_string(this->speed));
-  out.append("\n");
-
-  out.append("  direction: ");
-  out.append(proto_enum_to_string(this->direction));
-  out.append("\n");
-
-  out.append("  speed_level: ");
-  sprintf(buffer, "%" PRId32, this->speed_level);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  preset_mode: ");
-  out.append("'").append(this->preset_mode).append("'");
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -2066,66 +925,8 @@ void FanCommandRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_bool_field(total_size, 1, this->has_preset_mode, false);
   ProtoSize::add_string_field(total_size, 1, this->preset_mode, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void FanCommandRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("FanCommandRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_state: ");
-  out.append(YESNO(this->has_state));
-  out.append("\n");
-
-  out.append("  state: ");
-  out.append(YESNO(this->state));
-  out.append("\n");
-
-  out.append("  has_speed: ");
-  out.append(YESNO(this->has_speed));
-  out.append("\n");
-
-  out.append("  speed: ");
-  out.append(proto_enum_to_string(this->speed));
-  out.append("\n");
-
-  out.append("  has_oscillating: ");
-  out.append(YESNO(this->has_oscillating));
-  out.append("\n");
-
-  out.append("  oscillating: ");
-  out.append(YESNO(this->oscillating));
-  out.append("\n");
-
-  out.append("  has_direction: ");
-  out.append(YESNO(this->has_direction));
-  out.append("\n");
-
-  out.append("  direction: ");
-  out.append(proto_enum_to_string(this->direction));
-  out.append("\n");
-
-  out.append("  has_speed_level: ");
-  out.append(YESNO(this->has_speed_level));
-  out.append("\n");
-
-  out.append("  speed_level: ");
-  sprintf(buffer, "%" PRId32, this->speed_level);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_preset_mode: ");
-  out.append(YESNO(this->has_preset_mode));
-  out.append("\n");
-
-  out.append("  preset_mode: ");
-  out.append("'").append(this->preset_mode).append("'");
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_LIGHT
 bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 12: {
@@ -2256,84 +1057,6 @@ void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false);
   ProtoSize::add_uint32_field(total_size, 2, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesLightResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesLightResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  for (const auto &it : this->supported_color_modes) {
-    out.append("  supported_color_modes: ");
-    out.append(proto_enum_to_string(it));
-    out.append("\n");
-  }
-
-  out.append("  legacy_supports_brightness: ");
-  out.append(YESNO(this->legacy_supports_brightness));
-  out.append("\n");
-
-  out.append("  legacy_supports_rgb: ");
-  out.append(YESNO(this->legacy_supports_rgb));
-  out.append("\n");
-
-  out.append("  legacy_supports_white_value: ");
-  out.append(YESNO(this->legacy_supports_white_value));
-  out.append("\n");
-
-  out.append("  legacy_supports_color_temperature: ");
-  out.append(YESNO(this->legacy_supports_color_temperature));
-  out.append("\n");
-
-  out.append("  min_mireds: ");
-  sprintf(buffer, "%g", this->min_mireds);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  max_mireds: ");
-  sprintf(buffer, "%g", this->max_mireds);
-  out.append(buffer);
-  out.append("\n");
-
-  for (const auto &it : this->effects) {
-    out.append("  effects: ");
-    out.append("'").append(it).append("'");
-    out.append("\n");
-  }
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool LightStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -2434,74 +1157,6 @@ void LightStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->warm_white != 0.0f, false);
   ProtoSize::add_string_field(total_size, 1, this->effect, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void LightStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("LightStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  state: ");
-  out.append(YESNO(this->state));
-  out.append("\n");
-
-  out.append("  brightness: ");
-  sprintf(buffer, "%g", this->brightness);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  color_mode: ");
-  out.append(proto_enum_to_string(this->color_mode));
-  out.append("\n");
-
-  out.append("  color_brightness: ");
-  sprintf(buffer, "%g", this->color_brightness);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  red: ");
-  sprintf(buffer, "%g", this->red);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  green: ");
-  sprintf(buffer, "%g", this->green);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  blue: ");
-  sprintf(buffer, "%g", this->blue);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  white: ");
-  sprintf(buffer, "%g", this->white);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  color_temperature: ");
-  sprintf(buffer, "%g", this->color_temperature);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  cold_white: ");
-  sprintf(buffer, "%g", this->cold_white);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  warm_white: ");
-  sprintf(buffer, "%g", this->warm_white);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  effect: ");
-  out.append("'").append(this->effect).append("'");
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -2686,132 +1341,8 @@ void LightCommandRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_bool_field(total_size, 2, this->has_effect, false);
   ProtoSize::add_string_field(total_size, 2, this->effect, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void LightCommandRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("LightCommandRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_state: ");
-  out.append(YESNO(this->has_state));
-  out.append("\n");
-
-  out.append("  state: ");
-  out.append(YESNO(this->state));
-  out.append("\n");
-
-  out.append("  has_brightness: ");
-  out.append(YESNO(this->has_brightness));
-  out.append("\n");
-
-  out.append("  brightness: ");
-  sprintf(buffer, "%g", this->brightness);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_color_mode: ");
-  out.append(YESNO(this->has_color_mode));
-  out.append("\n");
-
-  out.append("  color_mode: ");
-  out.append(proto_enum_to_string(this->color_mode));
-  out.append("\n");
-
-  out.append("  has_color_brightness: ");
-  out.append(YESNO(this->has_color_brightness));
-  out.append("\n");
-
-  out.append("  color_brightness: ");
-  sprintf(buffer, "%g", this->color_brightness);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_rgb: ");
-  out.append(YESNO(this->has_rgb));
-  out.append("\n");
-
-  out.append("  red: ");
-  sprintf(buffer, "%g", this->red);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  green: ");
-  sprintf(buffer, "%g", this->green);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  blue: ");
-  sprintf(buffer, "%g", this->blue);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_white: ");
-  out.append(YESNO(this->has_white));
-  out.append("\n");
-
-  out.append("  white: ");
-  sprintf(buffer, "%g", this->white);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_color_temperature: ");
-  out.append(YESNO(this->has_color_temperature));
-  out.append("\n");
-
-  out.append("  color_temperature: ");
-  sprintf(buffer, "%g", this->color_temperature);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_cold_white: ");
-  out.append(YESNO(this->has_cold_white));
-  out.append("\n");
-
-  out.append("  cold_white: ");
-  sprintf(buffer, "%g", this->cold_white);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_warm_white: ");
-  out.append(YESNO(this->has_warm_white));
-  out.append("\n");
-
-  out.append("  warm_white: ");
-  sprintf(buffer, "%g", this->warm_white);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_transition_length: ");
-  out.append(YESNO(this->has_transition_length));
-  out.append("\n");
-
-  out.append("  transition_length: ");
-  sprintf(buffer, "%" PRIu32, this->transition_length);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_flash_length: ");
-  out.append(YESNO(this->has_flash_length));
-  out.append("\n");
-
-  out.append("  flash_length: ");
-  sprintf(buffer, "%" PRIu32, this->flash_length);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_effect: ");
-  out.append(YESNO(this->has_effect));
-  out.append("\n");
-
-  out.append("  effect: ");
-  out.append("'").append(this->effect).append("'");
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_SENSOR
 bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 7: {
@@ -2918,71 +1449,6 @@ void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesSensorResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesSensorResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  unit_of_measurement: ");
-  out.append("'").append(this->unit_of_measurement).append("'");
-  out.append("\n");
-
-  out.append("  accuracy_decimals: ");
-  sprintf(buffer, "%" PRId32, this->accuracy_decimals);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  force_update: ");
-  out.append(YESNO(this->force_update));
-  out.append("\n");
-
-  out.append("  device_class: ");
-  out.append("'").append(this->device_class).append("'");
-  out.append("\n");
-
-  out.append("  state_class: ");
-  out.append(proto_enum_to_string(this->state_class));
-  out.append("\n");
-
-  out.append("  legacy_last_reset_type: ");
-  out.append(proto_enum_to_string(this->legacy_last_reset_type));
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool SensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 3: {
@@ -3017,26 +1483,8 @@ void SensorStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f, false);
   ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void SensorStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("SensorStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  state: ");
-  sprintf(buffer, "%g", this->state);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  missing_state: ");
-  out.append(YESNO(this->missing_state));
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_SWITCH
 bool ListEntitiesSwitchResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 6: {
@@ -3119,54 +1567,6 @@ void ListEntitiesSwitchResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->device_class, false);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesSwitchResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesSwitchResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  assumed_state: ");
-  out.append(YESNO(this->assumed_state));
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  device_class: ");
-  out.append("'").append(this->device_class).append("'");
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool SwitchStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -3195,21 +1595,6 @@ void SwitchStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_bool_field(total_size, 1, this->state, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void SwitchStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("SwitchStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  state: ");
-  out.append(YESNO(this->state));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -3238,21 +1623,8 @@ void SwitchCommandRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_bool_field(total_size, 1, this->state, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void SwitchCommandRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("SwitchCommandRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  state: ");
-  out.append(YESNO(this->state));
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_TEXT_SENSOR
 bool ListEntitiesTextSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 6: {
@@ -3329,50 +1701,6 @@ void ListEntitiesTextSensorResponse::calculate_size(uint32_t &total_size) const
   ProtoSize::add_string_field(total_size, 1, this->device_class, false);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesTextSensorResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesTextSensorResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  device_class: ");
-  out.append("'").append(this->device_class).append("'");
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool TextSensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 3: {
@@ -3413,24 +1741,6 @@ void TextSensorStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->state, false);
   ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void TextSensorStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("TextSensorStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  state: ");
-  out.append("'").append(this->state).append("'");
-  out.append("\n");
-
-  out.append("  missing_state: ");
-  out.append(YESNO(this->missing_state));
-  out.append("\n");
-  out.append("}");
-}
 #endif
 bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
@@ -3454,20 +1764,6 @@ void SubscribeLogsRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->level), false);
   ProtoSize::add_bool_field(total_size, 1, this->dump_config, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void SubscribeLogsRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("SubscribeLogsRequest {\n");
-  out.append("  level: ");
-  out.append(proto_enum_to_string(this->level));
-  out.append("\n");
-
-  out.append("  dump_config: ");
-  out.append(YESNO(this->dump_config));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool SubscribeLogsResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -3502,24 +1798,7 @@ void SubscribeLogsResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->message, false);
   ProtoSize::add_bool_field(total_size, 1, this->send_failed, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void SubscribeLogsResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("SubscribeLogsResponse {\n");
-  out.append("  level: ");
-  out.append(proto_enum_to_string(this->level));
-  out.append("\n");
-
-  out.append("  message: ");
-  out.append(format_hex_pretty(this->message));
-  out.append("\n");
-
-  out.append("  send_failed: ");
-  out.append(YESNO(this->send_failed));
-  out.append("\n");
-  out.append("}");
-}
-#endif
+#ifdef USE_API_NOISE
 bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
   switch (field_id) {
     case 1: {
@@ -3536,16 +1815,6 @@ void NoiseEncryptionSetKeyRequest::encode(ProtoWriteBuffer buffer) const {
 void NoiseEncryptionSetKeyRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->key, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("NoiseEncryptionSetKeyRequest {\n");
-  out.append("  key: ");
-  out.append(format_hex_pretty(this->key));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool NoiseEncryptionSetKeyResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -3560,20 +1829,6 @@ void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer buffer) const { buff
 void NoiseEncryptionSetKeyResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_bool_field(total_size, 1, this->success, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("NoiseEncryptionSetKeyResponse {\n");
-  out.append("  success: ");
-  out.append(YESNO(this->success));
-  out.append("\n");
-  out.append("}");
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const {
-  out.append("SubscribeHomeassistantServicesRequest {}");
-}
 #endif
 bool HomeassistantServiceMap::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
   switch (field_id) {
@@ -3597,20 +1852,6 @@ void HomeassistantServiceMap::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->key, false);
   ProtoSize::add_string_field(total_size, 1, this->value, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void HomeassistantServiceMap::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("HomeassistantServiceMap {\n");
-  out.append("  key: ");
-  out.append("'").append(this->key).append("'");
-  out.append("\n");
-
-  out.append("  value: ");
-  out.append("'").append(this->value).append("'");
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool HomeassistantServiceResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 5: {
@@ -3663,43 +1904,6 @@ void HomeassistantServiceResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_repeated_message(total_size, 1, this->variables);
   ProtoSize::add_bool_field(total_size, 1, this->is_event, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void HomeassistantServiceResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("HomeassistantServiceResponse {\n");
-  out.append("  service: ");
-  out.append("'").append(this->service).append("'");
-  out.append("\n");
-
-  for (const auto &it : this->data) {
-    out.append("  data: ");
-    it.dump_to(out);
-    out.append("\n");
-  }
-
-  for (const auto &it : this->data_template) {
-    out.append("  data_template: ");
-    it.dump_to(out);
-    out.append("\n");
-  }
-
-  for (const auto &it : this->variables) {
-    out.append("  variables: ");
-    it.dump_to(out);
-    out.append("\n");
-  }
-
-  out.append("  is_event: ");
-  out.append(YESNO(this->is_event));
-  out.append("\n");
-  out.append("}");
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const {
-  out.append("SubscribeHomeAssistantStatesRequest {}");
-}
-#endif
 bool SubscribeHomeAssistantStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 3: {
@@ -3734,24 +1938,6 @@ void SubscribeHomeAssistantStateResponse::calculate_size(uint32_t &total_size) c
   ProtoSize::add_string_field(total_size, 1, this->attribute, false);
   ProtoSize::add_bool_field(total_size, 1, this->once, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("SubscribeHomeAssistantStateResponse {\n");
-  out.append("  entity_id: ");
-  out.append("'").append(this->entity_id).append("'");
-  out.append("\n");
-
-  out.append("  attribute: ");
-  out.append("'").append(this->attribute).append("'");
-  out.append("\n");
-
-  out.append("  once: ");
-  out.append(YESNO(this->once));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
   switch (field_id) {
     case 1: {
@@ -3780,27 +1966,6 @@ void HomeAssistantStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->state, false);
   ProtoSize::add_string_field(total_size, 1, this->attribute, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void HomeAssistantStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("HomeAssistantStateResponse {\n");
-  out.append("  entity_id: ");
-  out.append("'").append(this->entity_id).append("'");
-  out.append("\n");
-
-  out.append("  state: ");
-  out.append("'").append(this->state).append("'");
-  out.append("\n");
-
-  out.append("  attribute: ");
-  out.append("'").append(this->attribute).append("'");
-  out.append("\n");
-  out.append("}");
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); }
-#endif
 bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
   switch (field_id) {
     case 1: {
@@ -3815,17 +1980,6 @@ void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixe
 void GetTimeResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void GetTimeResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("GetTimeResponse {\n");
-  out.append("  epoch_seconds: ");
-  sprintf(buffer, "%" PRIu32, this->epoch_seconds);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool ListEntitiesServicesArgument::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -3854,20 +2008,6 @@ void ListEntitiesServicesArgument::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->name, false);
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->type), false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesServicesArgument::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesServicesArgument {\n");
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  type: ");
-  out.append(proto_enum_to_string(this->type));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool ListEntitiesServicesResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
   switch (field_id) {
     case 1: {
@@ -3904,27 +2044,6 @@ void ListEntitiesServicesResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_repeated_message(total_size, 1, this->args);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesServicesResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesServicesResponse {\n");
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  for (const auto &it : this->args) {
-    out.append("  args: ");
-    it.dump_to(out);
-    out.append("\n");
-  }
-  out.append("}");
-}
-#endif
 bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -4025,61 +2144,6 @@ void ExecuteServiceArgument::calculate_size(uint32_t &total_size) const {
     }
   }
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ExecuteServiceArgument::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ExecuteServiceArgument {\n");
-  out.append("  bool_: ");
-  out.append(YESNO(this->bool_));
-  out.append("\n");
-
-  out.append("  legacy_int: ");
-  sprintf(buffer, "%" PRId32, this->legacy_int);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  float_: ");
-  sprintf(buffer, "%g", this->float_);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  string_: ");
-  out.append("'").append(this->string_).append("'");
-  out.append("\n");
-
-  out.append("  int_: ");
-  sprintf(buffer, "%" PRId32, this->int_);
-  out.append(buffer);
-  out.append("\n");
-
-  for (const auto it : this->bool_array) {
-    out.append("  bool_array: ");
-    out.append(YESNO(it));
-    out.append("\n");
-  }
-
-  for (const auto &it : this->int_array) {
-    out.append("  int_array: ");
-    sprintf(buffer, "%" PRId32, it);
-    out.append(buffer);
-    out.append("\n");
-  }
-
-  for (const auto &it : this->float_array) {
-    out.append("  float_array: ");
-    sprintf(buffer, "%g", it);
-    out.append(buffer);
-    out.append("\n");
-  }
-
-  for (const auto &it : this->string_array) {
-    out.append("  string_array: ");
-    out.append("'").append(it).append("'");
-    out.append("\n");
-  }
-  out.append("}");
-}
-#endif
 bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
   switch (field_id) {
     case 2: {
@@ -4110,23 +2174,7 @@ void ExecuteServiceRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_repeated_message(total_size, 1, this->args);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ExecuteServiceRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ExecuteServiceRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  for (const auto &it : this->args) {
-    out.append("  args: ");
-    it.dump_to(out);
-    out.append("\n");
-  }
-  out.append("}");
-}
-#endif
+#ifdef USE_ESP32_CAMERA
 bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 5: {
@@ -4197,46 +2245,6 @@ void ListEntitiesCameraResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesCameraResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesCameraResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool CameraImageResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 3: {
@@ -4277,25 +2285,6 @@ void CameraImageResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->data, false);
   ProtoSize::add_bool_field(total_size, 1, this->done, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void CameraImageResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("CameraImageResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  data: ");
-  out.append(format_hex_pretty(this->data));
-  out.append("\n");
-
-  out.append("  done: ");
-  out.append(YESNO(this->done));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -4318,20 +2307,8 @@ void CameraImageRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_bool_field(total_size, 1, this->single, false);
   ProtoSize::add_bool_field(total_size, 1, this->stream, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void CameraImageRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("CameraImageRequest {\n");
-  out.append("  single: ");
-  out.append(YESNO(this->single));
-  out.append("\n");
-
-  out.append("  stream: ");
-  out.append(YESNO(this->stream));
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_CLIMATE
 bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 5: {
@@ -4546,136 +2523,6 @@ void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_max_humidity != 0.0f, false);
   ProtoSize::add_uint32_field(total_size, 2, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesClimateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesClimateResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  supports_current_temperature: ");
-  out.append(YESNO(this->supports_current_temperature));
-  out.append("\n");
-
-  out.append("  supports_two_point_target_temperature: ");
-  out.append(YESNO(this->supports_two_point_target_temperature));
-  out.append("\n");
-
-  for (const auto &it : this->supported_modes) {
-    out.append("  supported_modes: ");
-    out.append(proto_enum_to_string(it));
-    out.append("\n");
-  }
-
-  out.append("  visual_min_temperature: ");
-  sprintf(buffer, "%g", this->visual_min_temperature);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  visual_max_temperature: ");
-  sprintf(buffer, "%g", this->visual_max_temperature);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  visual_target_temperature_step: ");
-  sprintf(buffer, "%g", this->visual_target_temperature_step);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  legacy_supports_away: ");
-  out.append(YESNO(this->legacy_supports_away));
-  out.append("\n");
-
-  out.append("  supports_action: ");
-  out.append(YESNO(this->supports_action));
-  out.append("\n");
-
-  for (const auto &it : this->supported_fan_modes) {
-    out.append("  supported_fan_modes: ");
-    out.append(proto_enum_to_string(it));
-    out.append("\n");
-  }
-
-  for (const auto &it : this->supported_swing_modes) {
-    out.append("  supported_swing_modes: ");
-    out.append(proto_enum_to_string(it));
-    out.append("\n");
-  }
-
-  for (const auto &it : this->supported_custom_fan_modes) {
-    out.append("  supported_custom_fan_modes: ");
-    out.append("'").append(it).append("'");
-    out.append("\n");
-  }
-
-  for (const auto &it : this->supported_presets) {
-    out.append("  supported_presets: ");
-    out.append(proto_enum_to_string(it));
-    out.append("\n");
-  }
-
-  for (const auto &it : this->supported_custom_presets) {
-    out.append("  supported_custom_presets: ");
-    out.append("'").append(it).append("'");
-    out.append("\n");
-  }
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  visual_current_temperature_step: ");
-  sprintf(buffer, "%g", this->visual_current_temperature_step);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  supports_current_humidity: ");
-  out.append(YESNO(this->supports_current_humidity));
-  out.append("\n");
-
-  out.append("  supports_target_humidity: ");
-  out.append(YESNO(this->supports_target_humidity));
-  out.append("\n");
-
-  out.append("  visual_min_humidity: ");
-  sprintf(buffer, "%g", this->visual_min_humidity);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  visual_max_humidity: ");
-  sprintf(buffer, "%g", this->visual_max_humidity);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -4788,79 +2635,6 @@ void ClimateStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->current_humidity != 0.0f, false);
   ProtoSize::add_fixed_field<4>(total_size, 1, this->target_humidity != 0.0f, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ClimateStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ClimateStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  mode: ");
-  out.append(proto_enum_to_string(this->mode));
-  out.append("\n");
-
-  out.append("  current_temperature: ");
-  sprintf(buffer, "%g", this->current_temperature);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  target_temperature: ");
-  sprintf(buffer, "%g", this->target_temperature);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  target_temperature_low: ");
-  sprintf(buffer, "%g", this->target_temperature_low);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  target_temperature_high: ");
-  sprintf(buffer, "%g", this->target_temperature_high);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  unused_legacy_away: ");
-  out.append(YESNO(this->unused_legacy_away));
-  out.append("\n");
-
-  out.append("  action: ");
-  out.append(proto_enum_to_string(this->action));
-  out.append("\n");
-
-  out.append("  fan_mode: ");
-  out.append(proto_enum_to_string(this->fan_mode));
-  out.append("\n");
-
-  out.append("  swing_mode: ");
-  out.append(proto_enum_to_string(this->swing_mode));
-  out.append("\n");
-
-  out.append("  custom_fan_mode: ");
-  out.append("'").append(this->custom_fan_mode).append("'");
-  out.append("\n");
-
-  out.append("  preset: ");
-  out.append(proto_enum_to_string(this->preset));
-  out.append("\n");
-
-  out.append("  custom_preset: ");
-  out.append("'").append(this->custom_preset).append("'");
-  out.append("\n");
-
-  out.append("  current_humidity: ");
-  sprintf(buffer, "%g", this->current_humidity);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  target_humidity: ");
-  sprintf(buffer, "%g", this->target_humidity);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -5021,109 +2795,8 @@ void ClimateCommandRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_bool_field(total_size, 2, this->has_target_humidity, false);
   ProtoSize::add_fixed_field<4>(total_size, 2, this->target_humidity != 0.0f, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ClimateCommandRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ClimateCommandRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_mode: ");
-  out.append(YESNO(this->has_mode));
-  out.append("\n");
-
-  out.append("  mode: ");
-  out.append(proto_enum_to_string(this->mode));
-  out.append("\n");
-
-  out.append("  has_target_temperature: ");
-  out.append(YESNO(this->has_target_temperature));
-  out.append("\n");
-
-  out.append("  target_temperature: ");
-  sprintf(buffer, "%g", this->target_temperature);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_target_temperature_low: ");
-  out.append(YESNO(this->has_target_temperature_low));
-  out.append("\n");
-
-  out.append("  target_temperature_low: ");
-  sprintf(buffer, "%g", this->target_temperature_low);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_target_temperature_high: ");
-  out.append(YESNO(this->has_target_temperature_high));
-  out.append("\n");
-
-  out.append("  target_temperature_high: ");
-  sprintf(buffer, "%g", this->target_temperature_high);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  unused_has_legacy_away: ");
-  out.append(YESNO(this->unused_has_legacy_away));
-  out.append("\n");
-
-  out.append("  unused_legacy_away: ");
-  out.append(YESNO(this->unused_legacy_away));
-  out.append("\n");
-
-  out.append("  has_fan_mode: ");
-  out.append(YESNO(this->has_fan_mode));
-  out.append("\n");
-
-  out.append("  fan_mode: ");
-  out.append(proto_enum_to_string(this->fan_mode));
-  out.append("\n");
-
-  out.append("  has_swing_mode: ");
-  out.append(YESNO(this->has_swing_mode));
-  out.append("\n");
-
-  out.append("  swing_mode: ");
-  out.append(proto_enum_to_string(this->swing_mode));
-  out.append("\n");
-
-  out.append("  has_custom_fan_mode: ");
-  out.append(YESNO(this->has_custom_fan_mode));
-  out.append("\n");
-
-  out.append("  custom_fan_mode: ");
-  out.append("'").append(this->custom_fan_mode).append("'");
-  out.append("\n");
-
-  out.append("  has_preset: ");
-  out.append(YESNO(this->has_preset));
-  out.append("\n");
-
-  out.append("  preset: ");
-  out.append(proto_enum_to_string(this->preset));
-  out.append("\n");
-
-  out.append("  has_custom_preset: ");
-  out.append(YESNO(this->has_custom_preset));
-  out.append("\n");
-
-  out.append("  custom_preset: ");
-  out.append("'").append(this->custom_preset).append("'");
-  out.append("\n");
-
-  out.append("  has_target_humidity: ");
-  out.append(YESNO(this->has_target_humidity));
-  out.append("\n");
-
-  out.append("  target_humidity: ");
-  sprintf(buffer, "%g", this->target_humidity);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_NUMBER
 bool ListEntitiesNumberResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 9: {
@@ -5230,73 +2903,6 @@ void ListEntitiesNumberResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->device_class, false);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesNumberResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesNumberResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  min_value: ");
-  sprintf(buffer, "%g", this->min_value);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  max_value: ");
-  sprintf(buffer, "%g", this->max_value);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  step: ");
-  sprintf(buffer, "%g", this->step);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  unit_of_measurement: ");
-  out.append("'").append(this->unit_of_measurement).append("'");
-  out.append("\n");
-
-  out.append("  mode: ");
-  out.append(proto_enum_to_string(this->mode));
-  out.append("\n");
-
-  out.append("  device_class: ");
-  out.append("'").append(this->device_class).append("'");
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool NumberStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 3: {
@@ -5331,26 +2937,6 @@ void NumberStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f, false);
   ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void NumberStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("NumberStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  state: ");
-  sprintf(buffer, "%g", this->state);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  missing_state: ");
-  out.append(YESNO(this->missing_state));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
   switch (field_id) {
     case 1: {
@@ -5373,22 +2959,8 @@ void NumberCommandRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void NumberCommandRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("NumberCommandRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  state: ");
-  sprintf(buffer, "%g", this->state);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_SELECT
 bool ListEntitiesSelectResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 7: {
@@ -5471,52 +3043,6 @@ void ListEntitiesSelectResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesSelectResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesSelectResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  for (const auto &it : this->options) {
-    out.append("  options: ");
-    out.append("'").append(it).append("'");
-    out.append("\n");
-  }
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool SelectStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 3: {
@@ -5557,25 +3083,6 @@ void SelectStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->state, false);
   ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void SelectStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("SelectStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  state: ");
-  out.append("'").append(this->state).append("'");
-  out.append("\n");
-
-  out.append("  missing_state: ");
-  out.append(YESNO(this->missing_state));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
   switch (field_id) {
     case 2: {
@@ -5604,21 +3111,8 @@ void SelectCommandRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_string_field(total_size, 1, this->state, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void SelectCommandRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("SelectCommandRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  state: ");
-  out.append("'").append(this->state).append("'");
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_SIREN
 bool ListEntitiesSirenResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 6: {
@@ -5713,60 +3207,6 @@ void ListEntitiesSirenResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesSirenResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesSirenResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  for (const auto &it : this->tones) {
-    out.append("  tones: ");
-    out.append("'").append(it).append("'");
-    out.append("\n");
-  }
-
-  out.append("  supports_duration: ");
-  out.append(YESNO(this->supports_duration));
-  out.append("\n");
-
-  out.append("  supports_volume: ");
-  out.append(YESNO(this->supports_volume));
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool SirenStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -5795,21 +3235,6 @@ void SirenStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_bool_field(total_size, 1, this->state, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void SirenStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("SirenStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  state: ");
-  out.append(YESNO(this->state));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -5886,51 +3311,8 @@ void SirenCommandRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_bool_field(total_size, 1, this->has_volume, false);
   ProtoSize::add_fixed_field<4>(total_size, 1, this->volume != 0.0f, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void SirenCommandRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("SirenCommandRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_state: ");
-  out.append(YESNO(this->has_state));
-  out.append("\n");
-
-  out.append("  state: ");
-  out.append(YESNO(this->state));
-  out.append("\n");
-
-  out.append("  has_tone: ");
-  out.append(YESNO(this->has_tone));
-  out.append("\n");
-
-  out.append("  tone: ");
-  out.append("'").append(this->tone).append("'");
-  out.append("\n");
-
-  out.append("  has_duration: ");
-  out.append(YESNO(this->has_duration));
-  out.append("\n");
-
-  out.append("  duration: ");
-  sprintf(buffer, "%" PRIu32, this->duration);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_volume: ");
-  out.append(YESNO(this->has_volume));
-  out.append("\n");
-
-  out.append("  volume: ");
-  sprintf(buffer, "%g", this->volume);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_LOCK
 bool ListEntitiesLockResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 6: {
@@ -6025,62 +3407,6 @@ void ListEntitiesLockResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->code_format, false);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesLockResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesLockResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  assumed_state: ");
-  out.append(YESNO(this->assumed_state));
-  out.append("\n");
-
-  out.append("  supports_open: ");
-  out.append(YESNO(this->supports_open));
-  out.append("\n");
-
-  out.append("  requires_code: ");
-  out.append(YESNO(this->requires_code));
-  out.append("\n");
-
-  out.append("  code_format: ");
-  out.append("'").append(this->code_format).append("'");
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool LockStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -6109,21 +3435,6 @@ void LockStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->state), false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void LockStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("LockStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  state: ");
-  out.append(proto_enum_to_string(this->state));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -6170,29 +3481,8 @@ void LockCommandRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_bool_field(total_size, 1, this->has_code, false);
   ProtoSize::add_string_field(total_size, 1, this->code, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void LockCommandRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("LockCommandRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  command: ");
-  out.append(proto_enum_to_string(this->command));
-  out.append("\n");
-
-  out.append("  has_code: ");
-  out.append(YESNO(this->has_code));
-  out.append("\n");
-
-  out.append("  code: ");
-  out.append("'").append(this->code).append("'");
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_BUTTON
 bool ListEntitiesButtonResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 6: {
@@ -6269,50 +3559,6 @@ void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->device_class, false);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesButtonResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesButtonResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  device_class: ");
-  out.append("'").append(this->device_class).append("'");
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
   switch (field_id) {
     case 1: {
@@ -6327,17 +3573,8 @@ void ButtonCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode
 void ButtonCommandRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ButtonCommandRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ButtonCommandRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_MEDIA_PLAYER
 bool MediaPlayerSupportedFormat::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -6384,35 +3621,6 @@ void MediaPlayerSupportedFormat::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->purpose), false);
   ProtoSize::add_uint32_field(total_size, 1, this->sample_bytes, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void MediaPlayerSupportedFormat::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("MediaPlayerSupportedFormat {\n");
-  out.append("  format: ");
-  out.append("'").append(this->format).append("'");
-  out.append("\n");
-
-  out.append("  sample_rate: ");
-  sprintf(buffer, "%" PRIu32, this->sample_rate);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  num_channels: ");
-  sprintf(buffer, "%" PRIu32, this->num_channels);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  purpose: ");
-  out.append(proto_enum_to_string(this->purpose));
-  out.append("\n");
-
-  out.append("  sample_bytes: ");
-  sprintf(buffer, "%" PRIu32, this->sample_bytes);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 6: {
@@ -6497,56 +3705,6 @@ void ListEntitiesMediaPlayerResponse::calculate_size(uint32_t &total_size) const
   ProtoSize::add_repeated_message(total_size, 1, this->supported_formats);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesMediaPlayerResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  supports_pause: ");
-  out.append(YESNO(this->supports_pause));
-  out.append("\n");
-
-  for (const auto &it : this->supported_formats) {
-    out.append("  supported_formats: ");
-    it.dump_to(out);
-    out.append("\n");
-  }
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool MediaPlayerStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -6587,30 +3745,6 @@ void MediaPlayerStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->volume != 0.0f, false);
   ProtoSize::add_bool_field(total_size, 1, this->muted, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void MediaPlayerStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("MediaPlayerStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  state: ");
-  out.append(proto_enum_to_string(this->state));
-  out.append("\n");
-
-  out.append("  volume: ");
-  sprintf(buffer, "%g", this->volume);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  muted: ");
-  out.append(YESNO(this->muted));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -6687,50 +3821,8 @@ void MediaPlayerCommandRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_bool_field(total_size, 1, this->has_announcement, false);
   ProtoSize::add_bool_field(total_size, 1, this->announcement, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void MediaPlayerCommandRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("MediaPlayerCommandRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_command: ");
-  out.append(YESNO(this->has_command));
-  out.append("\n");
-
-  out.append("  command: ");
-  out.append(proto_enum_to_string(this->command));
-  out.append("\n");
-
-  out.append("  has_volume: ");
-  out.append(YESNO(this->has_volume));
-  out.append("\n");
-
-  out.append("  volume: ");
-  sprintf(buffer, "%g", this->volume);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_media_url: ");
-  out.append(YESNO(this->has_media_url));
-  out.append("\n");
-
-  out.append("  media_url: ");
-  out.append("'").append(this->media_url).append("'");
-  out.append("\n");
-
-  out.append("  has_announcement: ");
-  out.append(YESNO(this->has_announcement));
-  out.append("\n");
-
-  out.append("  announcement: ");
-  out.append(YESNO(this->announcement));
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_BLUETOOTH_PROXY
 bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -6747,17 +3839,6 @@ void SubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer)
 void SubscribeBluetoothLEAdvertisementsRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint32_field(total_size, 1, this->flags, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("SubscribeBluetoothLEAdvertisementsRequest {\n");
-  out.append("  flags: ");
-  sprintf(buffer, "%" PRIu32, this->flags);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothServiceData::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -6798,27 +3879,6 @@ void BluetoothServiceData::calculate_size(uint32_t &total_size) const {
   }
   ProtoSize::add_string_field(total_size, 1, this->data, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothServiceData::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothServiceData {\n");
-  out.append("  uuid: ");
-  out.append("'").append(this->uuid).append("'");
-  out.append("\n");
-
-  for (const auto &it : this->legacy_data) {
-    out.append("  legacy_data: ");
-    sprintf(buffer, "%" PRIu32, it);
-    out.append(buffer);
-    out.append("\n");
-  }
-
-  out.append("  data: ");
-  out.append(format_hex_pretty(this->data));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothLEAdvertisementResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -6887,49 +3947,6 @@ void BluetoothLEAdvertisementResponse::calculate_size(uint32_t &total_size) cons
   ProtoSize::add_repeated_message(total_size, 1, this->manufacturer_data);
   ProtoSize::add_uint32_field(total_size, 1, this->address_type, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothLEAdvertisementResponse {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append(format_hex_pretty(this->name));
-  out.append("\n");
-
-  out.append("  rssi: ");
-  sprintf(buffer, "%" PRId32, this->rssi);
-  out.append(buffer);
-  out.append("\n");
-
-  for (const auto &it : this->service_uuids) {
-    out.append("  service_uuids: ");
-    out.append("'").append(it).append("'");
-    out.append("\n");
-  }
-
-  for (const auto &it : this->service_data) {
-    out.append("  service_data: ");
-    it.dump_to(out);
-    out.append("\n");
-  }
-
-  for (const auto &it : this->manufacturer_data) {
-    out.append("  manufacturer_data: ");
-    it.dump_to(out);
-    out.append("\n");
-  }
-
-  out.append("  address_type: ");
-  sprintf(buffer, "%" PRIu32, this->address_type);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothLERawAdvertisement::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -6970,31 +3987,6 @@ void BluetoothLERawAdvertisement::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint32_field(total_size, 1, this->address_type, false);
   ProtoSize::add_string_field(total_size, 1, this->data, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothLERawAdvertisement::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothLERawAdvertisement {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  rssi: ");
-  sprintf(buffer, "%" PRId32, this->rssi);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  address_type: ");
-  sprintf(buffer, "%" PRIu32, this->address_type);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  data: ");
-  out.append(format_hex_pretty(this->data));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothLERawAdvertisementsResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
   switch (field_id) {
     case 1: {
@@ -7013,18 +4005,6 @@ void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const
 void BluetoothLERawAdvertisementsResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_repeated_message(total_size, 1, this->advertisements);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothLERawAdvertisementsResponse {\n");
-  for (const auto &it : this->advertisements) {
-    out.append("  advertisements: ");
-    it.dump_to(out);
-    out.append("\n");
-  }
-  out.append("}");
-}
-#endif
 bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7059,30 +4039,6 @@ void BluetoothDeviceRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_bool_field(total_size, 1, this->has_address_type, false);
   ProtoSize::add_uint32_field(total_size, 1, this->address_type, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothDeviceRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothDeviceRequest {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  request_type: ");
-  out.append(proto_enum_to_string(this->request_type));
-  out.append("\n");
-
-  out.append("  has_address_type: ");
-  out.append(YESNO(this->has_address_type));
-  out.append("\n");
-
-  out.append("  address_type: ");
-  sprintf(buffer, "%" PRIu32, this->address_type);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothDeviceConnectionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7117,31 +4073,6 @@ void BluetoothDeviceConnectionResponse::calculate_size(uint32_t &total_size) con
   ProtoSize::add_uint32_field(total_size, 1, this->mtu, false);
   ProtoSize::add_int32_field(total_size, 1, this->error, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothDeviceConnectionResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothDeviceConnectionResponse {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  connected: ");
-  out.append(YESNO(this->connected));
-  out.append("\n");
-
-  out.append("  mtu: ");
-  sprintf(buffer, "%" PRIu32, this->mtu);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  error: ");
-  sprintf(buffer, "%" PRId32, this->error);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7156,17 +4087,6 @@ void BluetoothGATTGetServicesRequest::encode(ProtoWriteBuffer buffer) const { bu
 void BluetoothGATTGetServicesRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint64_field(total_size, 1, this->address, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothGATTGetServicesRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothGATTGetServicesRequest {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothGATTDescriptor::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7195,24 +4115,6 @@ void BluetoothGATTDescriptor::calculate_size(uint32_t &total_size) const {
   }
   ProtoSize::add_uint32_field(total_size, 1, this->handle, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothGATTDescriptor::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothGATTDescriptor {\n");
-  for (const auto &it : this->uuid) {
-    out.append("  uuid: ");
-    sprintf(buffer, "%llu", it);
-    out.append(buffer);
-    out.append("\n");
-  }
-
-  out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothGATTCharacteristic::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7261,35 +4163,6 @@ void BluetoothGATTCharacteristic::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint32_field(total_size, 1, this->properties, false);
   ProtoSize::add_repeated_message(total_size, 1, this->descriptors);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothGATTCharacteristic::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothGATTCharacteristic {\n");
-  for (const auto &it : this->uuid) {
-    out.append("  uuid: ");
-    sprintf(buffer, "%llu", it);
-    out.append(buffer);
-    out.append("\n");
-  }
-
-  out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  properties: ");
-  sprintf(buffer, "%" PRIu32, this->properties);
-  out.append(buffer);
-  out.append("\n");
-
-  for (const auto &it : this->descriptors) {
-    out.append("  descriptors: ");
-    it.dump_to(out);
-    out.append("\n");
-  }
-  out.append("}");
-}
-#endif
 bool BluetoothGATTService::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7332,30 +4205,6 @@ void BluetoothGATTService::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint32_field(total_size, 1, this->handle, false);
   ProtoSize::add_repeated_message(total_size, 1, this->characteristics);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothGATTService::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothGATTService {\n");
-  for (const auto &it : this->uuid) {
-    out.append("  uuid: ");
-    sprintf(buffer, "%llu", it);
-    out.append(buffer);
-    out.append("\n");
-  }
-
-  out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
-  out.append(buffer);
-  out.append("\n");
-
-  for (const auto &it : this->characteristics) {
-    out.append("  characteristics: ");
-    it.dump_to(out);
-    out.append("\n");
-  }
-  out.append("}");
-}
-#endif
 bool BluetoothGATTGetServicesResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7386,23 +4235,6 @@ void BluetoothGATTGetServicesResponse::calculate_size(uint32_t &total_size) cons
   ProtoSize::add_uint64_field(total_size, 1, this->address, false);
   ProtoSize::add_repeated_message(total_size, 1, this->services);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothGATTGetServicesResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothGATTGetServicesResponse {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-
-  for (const auto &it : this->services) {
-    out.append("  services: ");
-    it.dump_to(out);
-    out.append("\n");
-  }
-  out.append("}");
-}
-#endif
 bool BluetoothGATTGetServicesDoneResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7419,17 +4251,6 @@ void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer buffer) const
 void BluetoothGATTGetServicesDoneResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint64_field(total_size, 1, this->address, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothGATTGetServicesDoneResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothGATTGetServicesDoneResponse {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7452,22 +4273,6 @@ void BluetoothGATTReadRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint64_field(total_size, 1, this->address, false);
   ProtoSize::add_uint32_field(total_size, 1, this->handle, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothGATTReadRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothGATTReadRequest {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothGATTReadResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7502,26 +4307,6 @@ void BluetoothGATTReadResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint32_field(total_size, 1, this->handle, false);
   ProtoSize::add_string_field(total_size, 1, this->data, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothGATTReadResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothGATTReadResponse {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  data: ");
-  out.append(format_hex_pretty(this->data));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7562,30 +4347,6 @@ void BluetoothGATTWriteRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_bool_field(total_size, 1, this->response, false);
   ProtoSize::add_string_field(total_size, 1, this->data, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothGATTWriteRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothGATTWriteRequest {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  response: ");
-  out.append(YESNO(this->response));
-  out.append("\n");
-
-  out.append("  data: ");
-  out.append(format_hex_pretty(this->data));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothGATTReadDescriptorRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7608,22 +4369,6 @@ void BluetoothGATTReadDescriptorRequest::calculate_size(uint32_t &total_size) co
   ProtoSize::add_uint64_field(total_size, 1, this->address, false);
   ProtoSize::add_uint32_field(total_size, 1, this->handle, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothGATTReadDescriptorRequest {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7658,26 +4403,6 @@ void BluetoothGATTWriteDescriptorRequest::calculate_size(uint32_t &total_size) c
   ProtoSize::add_uint32_field(total_size, 1, this->handle, false);
   ProtoSize::add_string_field(total_size, 1, this->data, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothGATTWriteDescriptorRequest {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  data: ");
-  out.append(format_hex_pretty(this->data));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7706,26 +4431,6 @@ void BluetoothGATTNotifyRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint32_field(total_size, 1, this->handle, false);
   ProtoSize::add_bool_field(total_size, 1, this->enable, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothGATTNotifyRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothGATTNotifyRequest {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  enable: ");
-  out.append(YESNO(this->enable));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothGATTNotifyDataResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7760,31 +4465,6 @@ void BluetoothGATTNotifyDataResponse::calculate_size(uint32_t &total_size) const
   ProtoSize::add_uint32_field(total_size, 1, this->handle, false);
   ProtoSize::add_string_field(total_size, 1, this->data, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothGATTNotifyDataResponse {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  data: ");
-  out.append(format_hex_pretty(this->data));
-  out.append("\n");
-  out.append("}");
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void SubscribeBluetoothConnectionsFreeRequest::dump_to(std::string &out) const {
-  out.append("SubscribeBluetoothConnectionsFreeRequest {}");
-}
-#endif
 bool BluetoothConnectionsFreeResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7819,29 +4499,6 @@ void BluetoothConnectionsFreeResponse::calculate_size(uint32_t &total_size) cons
     }
   }
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothConnectionsFreeResponse {\n");
-  out.append("  free: ");
-  sprintf(buffer, "%" PRIu32, this->free);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  limit: ");
-  sprintf(buffer, "%" PRIu32, this->limit);
-  out.append(buffer);
-  out.append("\n");
-
-  for (const auto &it : this->allocated) {
-    out.append("  allocated: ");
-    sprintf(buffer, "%llu", it);
-    out.append(buffer);
-    out.append("\n");
-  }
-  out.append("}");
-}
-#endif
 bool BluetoothGATTErrorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7870,27 +4527,6 @@ void BluetoothGATTErrorResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint32_field(total_size, 1, this->handle, false);
   ProtoSize::add_int32_field(total_size, 1, this->error, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothGATTErrorResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothGATTErrorResponse {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  error: ");
-  sprintf(buffer, "%" PRId32, this->error);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothGATTWriteResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7913,22 +4549,6 @@ void BluetoothGATTWriteResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint64_field(total_size, 1, this->address, false);
   ProtoSize::add_uint32_field(total_size, 1, this->handle, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothGATTWriteResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothGATTWriteResponse {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothGATTNotifyResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7951,22 +4571,6 @@ void BluetoothGATTNotifyResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint64_field(total_size, 1, this->address, false);
   ProtoSize::add_uint32_field(total_size, 1, this->handle, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothGATTNotifyResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothGATTNotifyResponse {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothDevicePairingResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -7995,26 +4599,6 @@ void BluetoothDevicePairingResponse::calculate_size(uint32_t &total_size) const
   ProtoSize::add_bool_field(total_size, 1, this->paired, false);
   ProtoSize::add_int32_field(total_size, 1, this->error, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothDevicePairingResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothDevicePairingResponse {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  paired: ");
-  out.append(YESNO(this->paired));
-  out.append("\n");
-
-  out.append("  error: ");
-  sprintf(buffer, "%" PRId32, this->error);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothDeviceUnpairingResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -8043,31 +4627,6 @@ void BluetoothDeviceUnpairingResponse::calculate_size(uint32_t &total_size) cons
   ProtoSize::add_bool_field(total_size, 1, this->success, false);
   ProtoSize::add_int32_field(total_size, 1, this->error, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothDeviceUnpairingResponse {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  success: ");
-  out.append(YESNO(this->success));
-  out.append("\n");
-
-  out.append("  error: ");
-  sprintf(buffer, "%" PRId32, this->error);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void UnsubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const {
-  out.append("UnsubscribeBluetoothLEAdvertisementsRequest {}");
-}
-#endif
 bool BluetoothDeviceClearCacheResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -8096,26 +4655,6 @@ void BluetoothDeviceClearCacheResponse::calculate_size(uint32_t &total_size) con
   ProtoSize::add_bool_field(total_size, 1, this->success, false);
   ProtoSize::add_int32_field(total_size, 1, this->error, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothDeviceClearCacheResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothDeviceClearCacheResponse {\n");
-  out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  success: ");
-  out.append(YESNO(this->success));
-  out.append("\n");
-
-  out.append("  error: ");
-  sprintf(buffer, "%" PRId32, this->error);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothScannerStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -8138,20 +4677,6 @@ void BluetoothScannerStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->state), false);
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothScannerStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothScannerStateResponse {\n");
-  out.append("  state: ");
-  out.append(proto_enum_to_string(this->state));
-  out.append("\n");
-
-  out.append("  mode: ");
-  out.append(proto_enum_to_string(this->mode));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -8168,16 +4693,8 @@ void BluetoothScannerSetModeRequest::encode(ProtoWriteBuffer buffer) const {
 void BluetoothScannerSetModeRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void BluetoothScannerSetModeRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("BluetoothScannerSetModeRequest {\n");
-  out.append("  mode: ");
-  out.append(proto_enum_to_string(this->mode));
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_VOICE_ASSISTANT
 bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -8200,21 +4717,6 @@ void SubscribeVoiceAssistantRequest::calculate_size(uint32_t &total_size) const
   ProtoSize::add_bool_field(total_size, 1, this->subscribe, false);
   ProtoSize::add_uint32_field(total_size, 1, this->flags, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void SubscribeVoiceAssistantRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("SubscribeVoiceAssistantRequest {\n");
-  out.append("  subscribe: ");
-  out.append(YESNO(this->subscribe));
-  out.append("\n");
-
-  out.append("  flags: ");
-  sprintf(buffer, "%" PRIu32, this->flags);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool VoiceAssistantAudioSettings::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -8249,27 +4751,6 @@ void VoiceAssistantAudioSettings::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint32_field(total_size, 1, this->auto_gain, false);
   ProtoSize::add_fixed_field<4>(total_size, 1, this->volume_multiplier != 0.0f, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void VoiceAssistantAudioSettings::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("VoiceAssistantAudioSettings {\n");
-  out.append("  noise_suppression_level: ");
-  sprintf(buffer, "%" PRIu32, this->noise_suppression_level);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  auto_gain: ");
-  sprintf(buffer, "%" PRIu32, this->auto_gain);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  volume_multiplier: ");
-  sprintf(buffer, "%g", this->volume_multiplier);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool VoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -8316,33 +4797,6 @@ void VoiceAssistantRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_message_object(total_size, 1, this->audio_settings, false);
   ProtoSize::add_string_field(total_size, 1, this->wake_word_phrase, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void VoiceAssistantRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("VoiceAssistantRequest {\n");
-  out.append("  start: ");
-  out.append(YESNO(this->start));
-  out.append("\n");
-
-  out.append("  conversation_id: ");
-  out.append("'").append(this->conversation_id).append("'");
-  out.append("\n");
-
-  out.append("  flags: ");
-  sprintf(buffer, "%" PRIu32, this->flags);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  audio_settings: ");
-  this->audio_settings.dump_to(out);
-  out.append("\n");
-
-  out.append("  wake_word_phrase: ");
-  out.append("'").append(this->wake_word_phrase).append("'");
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool VoiceAssistantResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -8365,21 +4819,6 @@ void VoiceAssistantResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint32_field(total_size, 1, this->port, false);
   ProtoSize::add_bool_field(total_size, 1, this->error, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void VoiceAssistantResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("VoiceAssistantResponse {\n");
-  out.append("  port: ");
-  sprintf(buffer, "%" PRIu32, this->port);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  error: ");
-  out.append(YESNO(this->error));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool VoiceAssistantEventData::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
   switch (field_id) {
     case 1: {
@@ -8402,20 +4841,6 @@ void VoiceAssistantEventData::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->name, false);
   ProtoSize::add_string_field(total_size, 1, this->value, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void VoiceAssistantEventData::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("VoiceAssistantEventData {\n");
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  value: ");
-  out.append("'").append(this->value).append("'");
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool VoiceAssistantEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -8446,22 +4871,6 @@ void VoiceAssistantEventResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->event_type), false);
   ProtoSize::add_repeated_message(total_size, 1, this->data);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void VoiceAssistantEventResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("VoiceAssistantEventResponse {\n");
-  out.append("  event_type: ");
-  out.append(proto_enum_to_string(this->event_type));
-  out.append("\n");
-
-  for (const auto &it : this->data) {
-    out.append("  data: ");
-    it.dump_to(out);
-    out.append("\n");
-  }
-  out.append("}");
-}
-#endif
 bool VoiceAssistantAudio::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -8490,20 +4899,6 @@ void VoiceAssistantAudio::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->data, false);
   ProtoSize::add_bool_field(total_size, 1, this->end, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void VoiceAssistantAudio::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("VoiceAssistantAudio {\n");
-  out.append("  data: ");
-  out.append(format_hex_pretty(this->data));
-  out.append("\n");
-
-  out.append("  end: ");
-  out.append(YESNO(this->end));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -8556,38 +4951,6 @@ void VoiceAssistantTimerEventResponse::calculate_size(uint32_t &total_size) cons
   ProtoSize::add_uint32_field(total_size, 1, this->seconds_left, false);
   ProtoSize::add_bool_field(total_size, 1, this->is_active, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("VoiceAssistantTimerEventResponse {\n");
-  out.append("  event_type: ");
-  out.append(proto_enum_to_string(this->event_type));
-  out.append("\n");
-
-  out.append("  timer_id: ");
-  out.append("'").append(this->timer_id).append("'");
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  total_seconds: ");
-  sprintf(buffer, "%" PRIu32, this->total_seconds);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  seconds_left: ");
-  sprintf(buffer, "%" PRIu32, this->seconds_left);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  is_active: ");
-  out.append(YESNO(this->is_active));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool VoiceAssistantAnnounceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 4: {
@@ -8628,28 +4991,6 @@ void VoiceAssistantAnnounceRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->preannounce_media_id, false);
   ProtoSize::add_bool_field(total_size, 1, this->start_conversation, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("VoiceAssistantAnnounceRequest {\n");
-  out.append("  media_id: ");
-  out.append("'").append(this->media_id).append("'");
-  out.append("\n");
-
-  out.append("  text: ");
-  out.append("'").append(this->text).append("'");
-  out.append("\n");
-
-  out.append("  preannounce_media_id: ");
-  out.append("'").append(this->preannounce_media_id).append("'");
-  out.append("\n");
-
-  out.append("  start_conversation: ");
-  out.append(YESNO(this->start_conversation));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool VoiceAssistantAnnounceFinished::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
@@ -8664,16 +5005,6 @@ void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer buffer) const { buf
 void VoiceAssistantAnnounceFinished::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_bool_field(total_size, 1, this->success, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("VoiceAssistantAnnounceFinished {\n");
-  out.append("  success: ");
-  out.append(YESNO(this->success));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool VoiceAssistantWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
   switch (field_id) {
     case 1: {
@@ -8708,31 +5039,6 @@ void VoiceAssistantWakeWord::calculate_size(uint32_t &total_size) const {
     }
   }
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void VoiceAssistantWakeWord::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("VoiceAssistantWakeWord {\n");
-  out.append("  id: ");
-  out.append("'").append(this->id).append("'");
-  out.append("\n");
-
-  out.append("  wake_word: ");
-  out.append("'").append(this->wake_word).append("'");
-  out.append("\n");
-
-  for (const auto &it : this->trained_languages) {
-    out.append("  trained_languages: ");
-    out.append("'").append(it).append("'");
-    out.append("\n");
-  }
-  out.append("}");
-}
-#endif
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const {
-  out.append("VoiceAssistantConfigurationRequest {}");
-}
-#endif
 bool VoiceAssistantConfigurationResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 3: {
@@ -8775,29 +5081,6 @@ void VoiceAssistantConfigurationResponse::calculate_size(uint32_t &total_size) c
   }
   ProtoSize::add_uint32_field(total_size, 1, this->max_active_wake_words, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("VoiceAssistantConfigurationResponse {\n");
-  for (const auto &it : this->available_wake_words) {
-    out.append("  available_wake_words: ");
-    it.dump_to(out);
-    out.append("\n");
-  }
-
-  for (const auto &it : this->active_wake_words) {
-    out.append("  active_wake_words: ");
-    out.append("'").append(it).append("'");
-    out.append("\n");
-  }
-
-  out.append("  max_active_wake_words: ");
-  sprintf(buffer, "%" PRIu32, this->max_active_wake_words);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
   switch (field_id) {
     case 1: {
@@ -8820,18 +5103,8 @@ void VoiceAssistantSetConfiguration::calculate_size(uint32_t &total_size) const
     }
   }
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void VoiceAssistantSetConfiguration::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("VoiceAssistantSetConfiguration {\n");
-  for (const auto &it : this->active_wake_words) {
-    out.append("  active_wake_words: ");
-    out.append("'").append(it).append("'");
-    out.append("\n");
-  }
-  out.append("}");
-}
 #endif
+#ifdef USE_ALARM_CONTROL_PANEL
 bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 6: {
@@ -8920,59 +5193,6 @@ void ListEntitiesAlarmControlPanelResponse::calculate_size(uint32_t &total_size)
   ProtoSize::add_bool_field(total_size, 1, this->requires_code_to_arm, false);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesAlarmControlPanelResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  supported_features: ");
-  sprintf(buffer, "%" PRIu32, this->supported_features);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  requires_code: ");
-  out.append(YESNO(this->requires_code));
-  out.append("\n");
-
-  out.append("  requires_code_to_arm: ");
-  out.append(YESNO(this->requires_code_to_arm));
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool AlarmControlPanelStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -9001,21 +5221,6 @@ void AlarmControlPanelStateResponse::calculate_size(uint32_t &total_size) const
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->state), false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void AlarmControlPanelStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("AlarmControlPanelStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  state: ");
-  out.append(proto_enum_to_string(this->state));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -9056,25 +5261,8 @@ void AlarmControlPanelCommandRequest::calculate_size(uint32_t &total_size) const
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->command), false);
   ProtoSize::add_string_field(total_size, 1, this->code, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void AlarmControlPanelCommandRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("AlarmControlPanelCommandRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  command: ");
-  out.append(proto_enum_to_string(this->command));
-  out.append("\n");
-
-  out.append("  code: ");
-  out.append("'").append(this->code).append("'");
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_TEXT
 bool ListEntitiesTextResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 6: {
@@ -9169,64 +5357,6 @@ void ListEntitiesTextResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesTextResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesTextResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  min_length: ");
-  sprintf(buffer, "%" PRIu32, this->min_length);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  max_length: ");
-  sprintf(buffer, "%" PRIu32, this->max_length);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  pattern: ");
-  out.append("'").append(this->pattern).append("'");
-  out.append("\n");
-
-  out.append("  mode: ");
-  out.append(proto_enum_to_string(this->mode));
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool TextStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 3: {
@@ -9267,25 +5397,6 @@ void TextStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->state, false);
   ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void TextStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("TextStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  state: ");
-  out.append("'").append(this->state).append("'");
-  out.append("\n");
-
-  out.append("  missing_state: ");
-  out.append(YESNO(this->missing_state));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool TextCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
   switch (field_id) {
     case 2: {
@@ -9314,21 +5425,8 @@ void TextCommandRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_string_field(total_size, 1, this->state, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void TextCommandRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("TextCommandRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  state: ");
-  out.append("'").append(this->state).append("'");
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_DATETIME_DATE
 bool ListEntitiesDateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 6: {
@@ -9399,46 +5497,6 @@ void ListEntitiesDateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesDateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesDateResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool DateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -9485,36 +5543,6 @@ void DateStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint32_field(total_size, 1, this->month, false);
   ProtoSize::add_uint32_field(total_size, 1, this->day, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void DateStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("DateStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  missing_state: ");
-  out.append(YESNO(this->missing_state));
-  out.append("\n");
-
-  out.append("  year: ");
-  sprintf(buffer, "%" PRIu32, this->year);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  month: ");
-  sprintf(buffer, "%" PRIu32, this->month);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  day: ");
-  sprintf(buffer, "%" PRIu32, this->day);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool DateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -9555,32 +5583,8 @@ void DateCommandRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint32_field(total_size, 1, this->month, false);
   ProtoSize::add_uint32_field(total_size, 1, this->day, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void DateCommandRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("DateCommandRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  year: ");
-  sprintf(buffer, "%" PRIu32, this->year);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  month: ");
-  sprintf(buffer, "%" PRIu32, this->month);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  day: ");
-  sprintf(buffer, "%" PRIu32, this->day);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_DATETIME_TIME
 bool ListEntitiesTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 6: {
@@ -9651,46 +5655,6 @@ void ListEntitiesTimeResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesTimeResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesTimeResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool TimeStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -9737,36 +5701,6 @@ void TimeStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint32_field(total_size, 1, this->minute, false);
   ProtoSize::add_uint32_field(total_size, 1, this->second, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void TimeStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("TimeStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  missing_state: ");
-  out.append(YESNO(this->missing_state));
-  out.append("\n");
-
-  out.append("  hour: ");
-  sprintf(buffer, "%" PRIu32, this->hour);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  minute: ");
-  sprintf(buffer, "%" PRIu32, this->minute);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  second: ");
-  sprintf(buffer, "%" PRIu32, this->second);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool TimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -9807,32 +5741,8 @@ void TimeCommandRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint32_field(total_size, 1, this->minute, false);
   ProtoSize::add_uint32_field(total_size, 1, this->second, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void TimeCommandRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("TimeCommandRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  hour: ");
-  sprintf(buffer, "%" PRIu32, this->hour);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  minute: ");
-  sprintf(buffer, "%" PRIu32, this->minute);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  second: ");
-  sprintf(buffer, "%" PRIu32, this->second);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_EVENT
 bool ListEntitiesEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 6: {
@@ -9921,56 +5831,6 @@ void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const {
   }
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesEventResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesEventResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  device_class: ");
-  out.append("'").append(this->device_class).append("'");
-  out.append("\n");
-
-  for (const auto &it : this->event_types) {
-    out.append("  event_types: ");
-    out.append("'").append(it).append("'");
-    out.append("\n");
-  }
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool EventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
   switch (field_id) {
     case 2: {
@@ -9999,21 +5859,8 @@ void EventResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_string_field(total_size, 1, this->event_type, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void EventResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("EventResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  event_type: ");
-  out.append("'").append(this->event_type).append("'");
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_VALVE
 bool ListEntitiesValveResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 6: {
@@ -10108,62 +5955,6 @@ void ListEntitiesValveResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_bool_field(total_size, 1, this->supports_stop, false);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesValveResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesValveResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  device_class: ");
-  out.append("'").append(this->device_class).append("'");
-  out.append("\n");
-
-  out.append("  assumed_state: ");
-  out.append(YESNO(this->assumed_state));
-  out.append("\n");
-
-  out.append("  supports_position: ");
-  out.append(YESNO(this->supports_position));
-  out.append("\n");
-
-  out.append("  supports_stop: ");
-  out.append(YESNO(this->supports_stop));
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool ValveStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 3: {
@@ -10198,26 +5989,6 @@ void ValveStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f, false);
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->current_operation), false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ValveStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ValveStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  position: ");
-  sprintf(buffer, "%g", this->position);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  current_operation: ");
-  out.append(proto_enum_to_string(this->current_operation));
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool ValveCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -10258,30 +6029,8 @@ void ValveCommandRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f, false);
   ProtoSize::add_bool_field(total_size, 1, this->stop, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ValveCommandRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ValveCommandRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  has_position: ");
-  out.append(YESNO(this->has_position));
-  out.append("\n");
-
-  out.append("  position: ");
-  sprintf(buffer, "%g", this->position);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  stop: ");
-  out.append(YESNO(this->stop));
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_DATETIME_DATETIME
 bool ListEntitiesDateTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 6: {
@@ -10352,46 +6101,6 @@ void ListEntitiesDateTimeResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesDateTimeResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesDateTimeResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool DateTimeStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -10426,26 +6135,6 @@ void DateTimeStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
   ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void DateTimeStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("DateTimeStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  missing_state: ");
-  out.append(YESNO(this->missing_state));
-  out.append("\n");
-
-  out.append("  epoch_seconds: ");
-  sprintf(buffer, "%" PRIu32, this->epoch_seconds);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
   switch (field_id) {
     case 1: {
@@ -10468,22 +6157,8 @@ void DateTimeCommandRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void DateTimeCommandRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("DateTimeCommandRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  epoch_seconds: ");
-  sprintf(buffer, "%" PRIu32, this->epoch_seconds);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
 #endif
+#ifdef USE_UPDATE
 bool ListEntitiesUpdateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 6: {
@@ -10560,50 +6235,6 @@ void ListEntitiesUpdateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->device_class, false);
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void ListEntitiesUpdateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("ListEntitiesUpdateResponse {\n");
-  out.append("  object_id: ");
-  out.append("'").append(this->object_id).append("'");
-  out.append("\n");
-
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  name: ");
-  out.append("'").append(this->name).append("'");
-  out.append("\n");
-
-  out.append("  unique_id: ");
-  out.append("'").append(this->unique_id).append("'");
-  out.append("\n");
-
-  out.append("  icon: ");
-  out.append("'").append(this->icon).append("'");
-  out.append("\n");
-
-  out.append("  disabled_by_default: ");
-  out.append(YESNO(this->disabled_by_default));
-  out.append("\n");
-
-  out.append("  entity_category: ");
-  out.append(proto_enum_to_string(this->entity_category));
-  out.append("\n");
-
-  out.append("  device_class: ");
-  out.append("'").append(this->device_class).append("'");
-  out.append("\n");
-
-  out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
-  out.append(buffer);
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool UpdateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -10686,54 +6317,6 @@ void UpdateStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->release_summary, false);
   ProtoSize::add_string_field(total_size, 1, this->release_url, false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void UpdateStateResponse::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("UpdateStateResponse {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  missing_state: ");
-  out.append(YESNO(this->missing_state));
-  out.append("\n");
-
-  out.append("  in_progress: ");
-  out.append(YESNO(this->in_progress));
-  out.append("\n");
-
-  out.append("  has_progress: ");
-  out.append(YESNO(this->has_progress));
-  out.append("\n");
-
-  out.append("  progress: ");
-  sprintf(buffer, "%g", this->progress);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  current_version: ");
-  out.append("'").append(this->current_version).append("'");
-  out.append("\n");
-
-  out.append("  latest_version: ");
-  out.append("'").append(this->latest_version).append("'");
-  out.append("\n");
-
-  out.append("  title: ");
-  out.append("'").append(this->title).append("'");
-  out.append("\n");
-
-  out.append("  release_summary: ");
-  out.append("'").append(this->release_summary).append("'");
-  out.append("\n");
-
-  out.append("  release_url: ");
-  out.append("'").append(this->release_url).append("'");
-  out.append("\n");
-  out.append("}");
-}
-#endif
 bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
@@ -10762,20 +6345,6 @@ void UpdateCommandRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->command), false);
 }
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void UpdateCommandRequest::dump_to(std::string &out) const {
-  __attribute__((unused)) char buffer[64];
-  out.append("UpdateCommandRequest {\n");
-  out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
-  out.append(buffer);
-  out.append("\n");
-
-  out.append("  command: ");
-  out.append(proto_enum_to_string(this->command));
-  out.append("\n");
-  out.append("}");
-}
 #endif
 
 }  // namespace api
diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h
index 2f0444c2cd..24b0e891c9 100644
--- a/esphome/components/api/api_pb2.h
+++ b/esphome/components/api/api_pb2.h
@@ -2,6 +2,8 @@
 // See script/api_protobuf/api_protobuf.py
 #pragma once
 
+#include "esphome/core/defines.h"
+
 #include "proto.h"
 #include "api_pb2_size.h"
 
@@ -15,6 +17,7 @@ enum EntityCategory : uint32_t {
   ENTITY_CATEGORY_CONFIG = 1,
   ENTITY_CATEGORY_DIAGNOSTIC = 2,
 };
+#ifdef USE_COVER
 enum LegacyCoverState : uint32_t {
   LEGACY_COVER_STATE_OPEN = 0,
   LEGACY_COVER_STATE_CLOSED = 1,
@@ -29,6 +32,8 @@ enum LegacyCoverCommand : uint32_t {
   LEGACY_COVER_COMMAND_CLOSE = 1,
   LEGACY_COVER_COMMAND_STOP = 2,
 };
+#endif
+#ifdef USE_FAN
 enum FanSpeed : uint32_t {
   FAN_SPEED_LOW = 0,
   FAN_SPEED_MEDIUM = 1,
@@ -38,6 +43,8 @@ enum FanDirection : uint32_t {
   FAN_DIRECTION_FORWARD = 0,
   FAN_DIRECTION_REVERSE = 1,
 };
+#endif
+#ifdef USE_LIGHT
 enum ColorMode : uint32_t {
   COLOR_MODE_UNKNOWN = 0,
   COLOR_MODE_ON_OFF = 1,
@@ -51,6 +58,8 @@ enum ColorMode : uint32_t {
   COLOR_MODE_RGB_COLOR_TEMPERATURE = 47,
   COLOR_MODE_RGB_COLD_WARM_WHITE = 51,
 };
+#endif
+#ifdef USE_SENSOR
 enum SensorStateClass : uint32_t {
   STATE_CLASS_NONE = 0,
   STATE_CLASS_MEASUREMENT = 1,
@@ -62,6 +71,7 @@ enum SensorLastResetType : uint32_t {
   LAST_RESET_NEVER = 1,
   LAST_RESET_AUTO = 2,
 };
+#endif
 enum LogLevel : uint32_t {
   LOG_LEVEL_NONE = 0,
   LOG_LEVEL_ERROR = 1,
@@ -82,6 +92,7 @@ enum ServiceArgType : uint32_t {
   SERVICE_ARG_TYPE_FLOAT_ARRAY = 6,
   SERVICE_ARG_TYPE_STRING_ARRAY = 7,
 };
+#ifdef USE_CLIMATE
 enum ClimateMode : uint32_t {
   CLIMATE_MODE_OFF = 0,
   CLIMATE_MODE_HEAT_COOL = 1,
@@ -127,11 +138,15 @@ enum ClimatePreset : uint32_t {
   CLIMATE_PRESET_SLEEP = 6,
   CLIMATE_PRESET_ACTIVITY = 7,
 };
+#endif
+#ifdef USE_NUMBER
 enum NumberMode : uint32_t {
   NUMBER_MODE_AUTO = 0,
   NUMBER_MODE_BOX = 1,
   NUMBER_MODE_SLIDER = 2,
 };
+#endif
+#ifdef USE_LOCK
 enum LockState : uint32_t {
   LOCK_STATE_NONE = 0,
   LOCK_STATE_LOCKED = 1,
@@ -145,6 +160,8 @@ enum LockCommand : uint32_t {
   LOCK_LOCK = 1,
   LOCK_OPEN = 2,
 };
+#endif
+#ifdef USE_MEDIA_PLAYER
 enum MediaPlayerState : uint32_t {
   MEDIA_PLAYER_STATE_NONE = 0,
   MEDIA_PLAYER_STATE_IDLE = 1,
@@ -162,6 +179,8 @@ enum MediaPlayerFormatPurpose : uint32_t {
   MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0,
   MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT = 1,
 };
+#endif
+#ifdef USE_BLUETOOTH_PROXY
 enum BluetoothDeviceRequestType : uint32_t {
   BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0,
   BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1,
@@ -183,6 +202,7 @@ enum BluetoothScannerMode : uint32_t {
   BLUETOOTH_SCANNER_MODE_PASSIVE = 0,
   BLUETOOTH_SCANNER_MODE_ACTIVE = 1,
 };
+#endif
 enum VoiceAssistantSubscribeFlag : uint32_t {
   VOICE_ASSISTANT_SUBSCRIBE_NONE = 0,
   VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1,
@@ -192,6 +212,7 @@ enum VoiceAssistantRequestFlag : uint32_t {
   VOICE_ASSISTANT_REQUEST_USE_VAD = 1,
   VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD = 2,
 };
+#ifdef USE_VOICE_ASSISTANT
 enum VoiceAssistantEvent : uint32_t {
   VOICE_ASSISTANT_ERROR = 0,
   VOICE_ASSISTANT_RUN_START = 1,
@@ -216,6 +237,8 @@ enum VoiceAssistantTimerEvent : uint32_t {
   VOICE_ASSISTANT_TIMER_CANCELLED = 2,
   VOICE_ASSISTANT_TIMER_FINISHED = 3,
 };
+#endif
+#ifdef USE_ALARM_CONTROL_PANEL
 enum AlarmControlPanelState : uint32_t {
   ALARM_STATE_DISARMED = 0,
   ALARM_STATE_ARMED_HOME = 1,
@@ -237,20 +260,27 @@ enum AlarmControlPanelStateCommand : uint32_t {
   ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS = 5,
   ALARM_CONTROL_PANEL_TRIGGER = 6,
 };
+#endif
+#ifdef USE_TEXT
 enum TextMode : uint32_t {
   TEXT_MODE_TEXT = 0,
   TEXT_MODE_PASSWORD = 1,
 };
+#endif
+#ifdef USE_VALVE
 enum ValveOperation : uint32_t {
   VALVE_OPERATION_IDLE = 0,
   VALVE_OPERATION_IS_OPENING = 1,
   VALVE_OPERATION_IS_CLOSING = 2,
 };
+#endif
+#ifdef USE_UPDATE
 enum UpdateCommand : uint32_t {
   UPDATE_COMMAND_NONE = 0,
   UPDATE_COMMAND_UPDATE = 1,
   UPDATE_COMMAND_CHECK = 2,
 };
+#endif
 
 }  // namespace enums
 
@@ -523,6 +553,7 @@ class SubscribeStatesRequest : public ProtoMessage {
 
  protected:
 };
+#ifdef USE_BINARY_SENSOR
 class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 12;
@@ -562,6 +593,8 @@ class BinarySensorStateResponse : public StateResponseProtoMessage {
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
+#ifdef USE_COVER
 class ListEntitiesCoverResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 13;
@@ -631,6 +664,8 @@ class CoverCommandRequest : public ProtoMessage {
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
+#ifdef USE_FAN
 class ListEntitiesFanResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 14;
@@ -709,6 +744,8 @@ class FanCommandRequest : public ProtoMessage {
   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
+#ifdef USE_LIGHT
 class ListEntitiesLightResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 15;
@@ -810,6 +847,8 @@ class LightCommandRequest : public ProtoMessage {
   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
+#ifdef USE_SENSOR
 class ListEntitiesSensorResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 16;
@@ -853,6 +892,8 @@ class SensorStateResponse : public StateResponseProtoMessage {
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
+#ifdef USE_SWITCH
 class ListEntitiesSwitchResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 17;
@@ -910,6 +951,8 @@ class SwitchCommandRequest : public ProtoMessage {
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
+#ifdef USE_TEXT_SENSOR
 class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 18;
@@ -949,6 +992,7 @@ class TextSensorStateResponse : public StateResponseProtoMessage {
   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
 class SubscribeLogsRequest : public ProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 28;
@@ -987,6 +1031,7 @@ class SubscribeLogsResponse : public ProtoMessage {
   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#ifdef USE_API_NOISE
 class NoiseEncryptionSetKeyRequest : public ProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 124;
@@ -1021,6 +1066,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage {
  protected:
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
 class SubscribeHomeassistantServicesRequest : public ProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 34;
@@ -1226,6 +1272,7 @@ class ExecuteServiceRequest : public ProtoMessage {
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
 };
+#ifdef USE_ESP32_CAMERA
 class ListEntitiesCameraResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 43;
@@ -1283,6 +1330,8 @@ class CameraImageRequest : public ProtoMessage {
  protected:
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
+#ifdef USE_CLIMATE
 class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 46;
@@ -1392,6 +1441,8 @@ class ClimateCommandRequest : public ProtoMessage {
   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
+#ifdef USE_NUMBER
 class ListEntitiesNumberResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 49;
@@ -1453,6 +1504,8 @@ class NumberCommandRequest : public ProtoMessage {
  protected:
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
 };
+#endif
+#ifdef USE_SELECT
 class ListEntitiesSelectResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 52;
@@ -1511,6 +1564,8 @@ class SelectCommandRequest : public ProtoMessage {
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
 };
+#endif
+#ifdef USE_SIREN
 class ListEntitiesSirenResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 55;
@@ -1577,6 +1632,8 @@ class SirenCommandRequest : public ProtoMessage {
   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
+#ifdef USE_LOCK
 class ListEntitiesLockResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 58;
@@ -1639,6 +1696,8 @@ class LockCommandRequest : public ProtoMessage {
   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
+#ifdef USE_BUTTON
 class ListEntitiesButtonResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 61;
@@ -1675,6 +1734,8 @@ class ButtonCommandRequest : public ProtoMessage {
  protected:
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
 };
+#endif
+#ifdef USE_MEDIA_PLAYER
 class MediaPlayerSupportedFormat : public ProtoMessage {
  public:
   std::string format{};
@@ -1759,6 +1820,8 @@ class MediaPlayerCommandRequest : public ProtoMessage {
   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
+#ifdef USE_BLUETOOTH_PROXY
 class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 66;
@@ -2313,6 +2376,8 @@ class BluetoothScannerSetModeRequest : public ProtoMessage {
  protected:
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
+#ifdef USE_VOICE_ASSISTANT
 class SubscribeVoiceAssistantRequest : public ProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 89;
@@ -2562,6 +2627,8 @@ class VoiceAssistantSetConfiguration : public ProtoMessage {
  protected:
   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
 };
+#endif
+#ifdef USE_ALARM_CONTROL_PANEL
 class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 94;
@@ -2622,6 +2689,8 @@ class AlarmControlPanelCommandRequest : public ProtoMessage {
   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
+#ifdef USE_TEXT
 class ListEntitiesTextResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 97;
@@ -2683,6 +2752,8 @@ class TextCommandRequest : public ProtoMessage {
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
 };
+#endif
+#ifdef USE_DATETIME_DATE
 class ListEntitiesDateResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 100;
@@ -2743,6 +2814,8 @@ class DateCommandRequest : public ProtoMessage {
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
+#ifdef USE_DATETIME_TIME
 class ListEntitiesTimeResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 103;
@@ -2803,6 +2876,8 @@ class TimeCommandRequest : public ProtoMessage {
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
+#ifdef USE_EVENT
 class ListEntitiesEventResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 107;
@@ -2841,6 +2916,8 @@ class EventResponse : public StateResponseProtoMessage {
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
 };
+#endif
+#ifdef USE_VALVE
 class ListEntitiesValveResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 109;
@@ -2903,6 +2980,8 @@ class ValveCommandRequest : public ProtoMessage {
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
+#ifdef USE_DATETIME_DATETIME
 class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 112;
@@ -2958,6 +3037,8 @@ class DateTimeCommandRequest : public ProtoMessage {
  protected:
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
 };
+#endif
+#ifdef USE_UPDATE
 class ListEntitiesUpdateResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 116;
@@ -3023,6 +3104,7 @@ class UpdateCommandRequest : public ProtoMessage {
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+#endif
 
 }  // namespace api
 }  // namespace esphome
diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp
new file mode 100644
index 0000000000..6658fd754b
--- /dev/null
+++ b/esphome/components/api/api_pb2_dump.cpp
@@ -0,0 +1,4228 @@
+// This file was automatically generated with a tool.
+// See script/api_protobuf/api_protobuf.py
+#include "api_pb2.h"
+#include "esphome/core/helpers.h"
+
+#include 
+
+#ifdef HAS_PROTO_MESSAGE_DUMP
+
+namespace esphome {
+namespace api {
+
+template<> const char *proto_enum_to_string(enums::EntityCategory value) {
+  switch (value) {
+    case enums::ENTITY_CATEGORY_NONE:
+      return "ENTITY_CATEGORY_NONE";
+    case enums::ENTITY_CATEGORY_CONFIG:
+      return "ENTITY_CATEGORY_CONFIG";
+    case enums::ENTITY_CATEGORY_DIAGNOSTIC:
+      return "ENTITY_CATEGORY_DIAGNOSTIC";
+    default:
+      return "UNKNOWN";
+  }
+}
+#ifdef USE_COVER
+template<> const char *proto_enum_to_string(enums::LegacyCoverState value) {
+  switch (value) {
+    case enums::LEGACY_COVER_STATE_OPEN:
+      return "LEGACY_COVER_STATE_OPEN";
+    case enums::LEGACY_COVER_STATE_CLOSED:
+      return "LEGACY_COVER_STATE_CLOSED";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string(enums::CoverOperation value) {
+  switch (value) {
+    case enums::COVER_OPERATION_IDLE:
+      return "COVER_OPERATION_IDLE";
+    case enums::COVER_OPERATION_IS_OPENING:
+      return "COVER_OPERATION_IS_OPENING";
+    case enums::COVER_OPERATION_IS_CLOSING:
+      return "COVER_OPERATION_IS_CLOSING";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string(enums::LegacyCoverCommand value) {
+  switch (value) {
+    case enums::LEGACY_COVER_COMMAND_OPEN:
+      return "LEGACY_COVER_COMMAND_OPEN";
+    case enums::LEGACY_COVER_COMMAND_CLOSE:
+      return "LEGACY_COVER_COMMAND_CLOSE";
+    case enums::LEGACY_COVER_COMMAND_STOP:
+      return "LEGACY_COVER_COMMAND_STOP";
+    default:
+      return "UNKNOWN";
+  }
+}
+#endif
+#ifdef USE_FAN
+template<> const char *proto_enum_to_string(enums::FanSpeed value) {
+  switch (value) {
+    case enums::FAN_SPEED_LOW:
+      return "FAN_SPEED_LOW";
+    case enums::FAN_SPEED_MEDIUM:
+      return "FAN_SPEED_MEDIUM";
+    case enums::FAN_SPEED_HIGH:
+      return "FAN_SPEED_HIGH";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string(enums::FanDirection value) {
+  switch (value) {
+    case enums::FAN_DIRECTION_FORWARD:
+      return "FAN_DIRECTION_FORWARD";
+    case enums::FAN_DIRECTION_REVERSE:
+      return "FAN_DIRECTION_REVERSE";
+    default:
+      return "UNKNOWN";
+  }
+}
+#endif
+#ifdef USE_LIGHT
+template<> const char *proto_enum_to_string(enums::ColorMode value) {
+  switch (value) {
+    case enums::COLOR_MODE_UNKNOWN:
+      return "COLOR_MODE_UNKNOWN";
+    case enums::COLOR_MODE_ON_OFF:
+      return "COLOR_MODE_ON_OFF";
+    case enums::COLOR_MODE_LEGACY_BRIGHTNESS:
+      return "COLOR_MODE_LEGACY_BRIGHTNESS";
+    case enums::COLOR_MODE_BRIGHTNESS:
+      return "COLOR_MODE_BRIGHTNESS";
+    case enums::COLOR_MODE_WHITE:
+      return "COLOR_MODE_WHITE";
+    case enums::COLOR_MODE_COLOR_TEMPERATURE:
+      return "COLOR_MODE_COLOR_TEMPERATURE";
+    case enums::COLOR_MODE_COLD_WARM_WHITE:
+      return "COLOR_MODE_COLD_WARM_WHITE";
+    case enums::COLOR_MODE_RGB:
+      return "COLOR_MODE_RGB";
+    case enums::COLOR_MODE_RGB_WHITE:
+      return "COLOR_MODE_RGB_WHITE";
+    case enums::COLOR_MODE_RGB_COLOR_TEMPERATURE:
+      return "COLOR_MODE_RGB_COLOR_TEMPERATURE";
+    case enums::COLOR_MODE_RGB_COLD_WARM_WHITE:
+      return "COLOR_MODE_RGB_COLD_WARM_WHITE";
+    default:
+      return "UNKNOWN";
+  }
+}
+#endif
+#ifdef USE_SENSOR
+template<> const char *proto_enum_to_string(enums::SensorStateClass value) {
+  switch (value) {
+    case enums::STATE_CLASS_NONE:
+      return "STATE_CLASS_NONE";
+    case enums::STATE_CLASS_MEASUREMENT:
+      return "STATE_CLASS_MEASUREMENT";
+    case enums::STATE_CLASS_TOTAL_INCREASING:
+      return "STATE_CLASS_TOTAL_INCREASING";
+    case enums::STATE_CLASS_TOTAL:
+      return "STATE_CLASS_TOTAL";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string(enums::SensorLastResetType value) {
+  switch (value) {
+    case enums::LAST_RESET_NONE:
+      return "LAST_RESET_NONE";
+    case enums::LAST_RESET_NEVER:
+      return "LAST_RESET_NEVER";
+    case enums::LAST_RESET_AUTO:
+      return "LAST_RESET_AUTO";
+    default:
+      return "UNKNOWN";
+  }
+}
+#endif
+template<> const char *proto_enum_to_string(enums::LogLevel value) {
+  switch (value) {
+    case enums::LOG_LEVEL_NONE:
+      return "LOG_LEVEL_NONE";
+    case enums::LOG_LEVEL_ERROR:
+      return "LOG_LEVEL_ERROR";
+    case enums::LOG_LEVEL_WARN:
+      return "LOG_LEVEL_WARN";
+    case enums::LOG_LEVEL_INFO:
+      return "LOG_LEVEL_INFO";
+    case enums::LOG_LEVEL_CONFIG:
+      return "LOG_LEVEL_CONFIG";
+    case enums::LOG_LEVEL_DEBUG:
+      return "LOG_LEVEL_DEBUG";
+    case enums::LOG_LEVEL_VERBOSE:
+      return "LOG_LEVEL_VERBOSE";
+    case enums::LOG_LEVEL_VERY_VERBOSE:
+      return "LOG_LEVEL_VERY_VERBOSE";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string(enums::ServiceArgType value) {
+  switch (value) {
+    case enums::SERVICE_ARG_TYPE_BOOL:
+      return "SERVICE_ARG_TYPE_BOOL";
+    case enums::SERVICE_ARG_TYPE_INT:
+      return "SERVICE_ARG_TYPE_INT";
+    case enums::SERVICE_ARG_TYPE_FLOAT:
+      return "SERVICE_ARG_TYPE_FLOAT";
+    case enums::SERVICE_ARG_TYPE_STRING:
+      return "SERVICE_ARG_TYPE_STRING";
+    case enums::SERVICE_ARG_TYPE_BOOL_ARRAY:
+      return "SERVICE_ARG_TYPE_BOOL_ARRAY";
+    case enums::SERVICE_ARG_TYPE_INT_ARRAY:
+      return "SERVICE_ARG_TYPE_INT_ARRAY";
+    case enums::SERVICE_ARG_TYPE_FLOAT_ARRAY:
+      return "SERVICE_ARG_TYPE_FLOAT_ARRAY";
+    case enums::SERVICE_ARG_TYPE_STRING_ARRAY:
+      return "SERVICE_ARG_TYPE_STRING_ARRAY";
+    default:
+      return "UNKNOWN";
+  }
+}
+#ifdef USE_CLIMATE
+template<> const char *proto_enum_to_string(enums::ClimateMode value) {
+  switch (value) {
+    case enums::CLIMATE_MODE_OFF:
+      return "CLIMATE_MODE_OFF";
+    case enums::CLIMATE_MODE_HEAT_COOL:
+      return "CLIMATE_MODE_HEAT_COOL";
+    case enums::CLIMATE_MODE_COOL:
+      return "CLIMATE_MODE_COOL";
+    case enums::CLIMATE_MODE_HEAT:
+      return "CLIMATE_MODE_HEAT";
+    case enums::CLIMATE_MODE_FAN_ONLY:
+      return "CLIMATE_MODE_FAN_ONLY";
+    case enums::CLIMATE_MODE_DRY:
+      return "CLIMATE_MODE_DRY";
+    case enums::CLIMATE_MODE_AUTO:
+      return "CLIMATE_MODE_AUTO";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string(enums::ClimateFanMode value) {
+  switch (value) {
+    case enums::CLIMATE_FAN_ON:
+      return "CLIMATE_FAN_ON";
+    case enums::CLIMATE_FAN_OFF:
+      return "CLIMATE_FAN_OFF";
+    case enums::CLIMATE_FAN_AUTO:
+      return "CLIMATE_FAN_AUTO";
+    case enums::CLIMATE_FAN_LOW:
+      return "CLIMATE_FAN_LOW";
+    case enums::CLIMATE_FAN_MEDIUM:
+      return "CLIMATE_FAN_MEDIUM";
+    case enums::CLIMATE_FAN_HIGH:
+      return "CLIMATE_FAN_HIGH";
+    case enums::CLIMATE_FAN_MIDDLE:
+      return "CLIMATE_FAN_MIDDLE";
+    case enums::CLIMATE_FAN_FOCUS:
+      return "CLIMATE_FAN_FOCUS";
+    case enums::CLIMATE_FAN_DIFFUSE:
+      return "CLIMATE_FAN_DIFFUSE";
+    case enums::CLIMATE_FAN_QUIET:
+      return "CLIMATE_FAN_QUIET";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string(enums::ClimateSwingMode value) {
+  switch (value) {
+    case enums::CLIMATE_SWING_OFF:
+      return "CLIMATE_SWING_OFF";
+    case enums::CLIMATE_SWING_BOTH:
+      return "CLIMATE_SWING_BOTH";
+    case enums::CLIMATE_SWING_VERTICAL:
+      return "CLIMATE_SWING_VERTICAL";
+    case enums::CLIMATE_SWING_HORIZONTAL:
+      return "CLIMATE_SWING_HORIZONTAL";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string(enums::ClimateAction value) {
+  switch (value) {
+    case enums::CLIMATE_ACTION_OFF:
+      return "CLIMATE_ACTION_OFF";
+    case enums::CLIMATE_ACTION_COOLING:
+      return "CLIMATE_ACTION_COOLING";
+    case enums::CLIMATE_ACTION_HEATING:
+      return "CLIMATE_ACTION_HEATING";
+    case enums::CLIMATE_ACTION_IDLE:
+      return "CLIMATE_ACTION_IDLE";
+    case enums::CLIMATE_ACTION_DRYING:
+      return "CLIMATE_ACTION_DRYING";
+    case enums::CLIMATE_ACTION_FAN:
+      return "CLIMATE_ACTION_FAN";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string(enums::ClimatePreset value) {
+  switch (value) {
+    case enums::CLIMATE_PRESET_NONE:
+      return "CLIMATE_PRESET_NONE";
+    case enums::CLIMATE_PRESET_HOME:
+      return "CLIMATE_PRESET_HOME";
+    case enums::CLIMATE_PRESET_AWAY:
+      return "CLIMATE_PRESET_AWAY";
+    case enums::CLIMATE_PRESET_BOOST:
+      return "CLIMATE_PRESET_BOOST";
+    case enums::CLIMATE_PRESET_COMFORT:
+      return "CLIMATE_PRESET_COMFORT";
+    case enums::CLIMATE_PRESET_ECO:
+      return "CLIMATE_PRESET_ECO";
+    case enums::CLIMATE_PRESET_SLEEP:
+      return "CLIMATE_PRESET_SLEEP";
+    case enums::CLIMATE_PRESET_ACTIVITY:
+      return "CLIMATE_PRESET_ACTIVITY";
+    default:
+      return "UNKNOWN";
+  }
+}
+#endif
+#ifdef USE_NUMBER
+template<> const char *proto_enum_to_string(enums::NumberMode value) {
+  switch (value) {
+    case enums::NUMBER_MODE_AUTO:
+      return "NUMBER_MODE_AUTO";
+    case enums::NUMBER_MODE_BOX:
+      return "NUMBER_MODE_BOX";
+    case enums::NUMBER_MODE_SLIDER:
+      return "NUMBER_MODE_SLIDER";
+    default:
+      return "UNKNOWN";
+  }
+}
+#endif
+#ifdef USE_LOCK
+template<> const char *proto_enum_to_string(enums::LockState value) {
+  switch (value) {
+    case enums::LOCK_STATE_NONE:
+      return "LOCK_STATE_NONE";
+    case enums::LOCK_STATE_LOCKED:
+      return "LOCK_STATE_LOCKED";
+    case enums::LOCK_STATE_UNLOCKED:
+      return "LOCK_STATE_UNLOCKED";
+    case enums::LOCK_STATE_JAMMED:
+      return "LOCK_STATE_JAMMED";
+    case enums::LOCK_STATE_LOCKING:
+      return "LOCK_STATE_LOCKING";
+    case enums::LOCK_STATE_UNLOCKING:
+      return "LOCK_STATE_UNLOCKING";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string(enums::LockCommand value) {
+  switch (value) {
+    case enums::LOCK_UNLOCK:
+      return "LOCK_UNLOCK";
+    case enums::LOCK_LOCK:
+      return "LOCK_LOCK";
+    case enums::LOCK_OPEN:
+      return "LOCK_OPEN";
+    default:
+      return "UNKNOWN";
+  }
+}
+#endif
+#ifdef USE_MEDIA_PLAYER
+template<> const char *proto_enum_to_string(enums::MediaPlayerState value) {
+  switch (value) {
+    case enums::MEDIA_PLAYER_STATE_NONE:
+      return "MEDIA_PLAYER_STATE_NONE";
+    case enums::MEDIA_PLAYER_STATE_IDLE:
+      return "MEDIA_PLAYER_STATE_IDLE";
+    case enums::MEDIA_PLAYER_STATE_PLAYING:
+      return "MEDIA_PLAYER_STATE_PLAYING";
+    case enums::MEDIA_PLAYER_STATE_PAUSED:
+      return "MEDIA_PLAYER_STATE_PAUSED";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string(enums::MediaPlayerCommand value) {
+  switch (value) {
+    case enums::MEDIA_PLAYER_COMMAND_PLAY:
+      return "MEDIA_PLAYER_COMMAND_PLAY";
+    case enums::MEDIA_PLAYER_COMMAND_PAUSE:
+      return "MEDIA_PLAYER_COMMAND_PAUSE";
+    case enums::MEDIA_PLAYER_COMMAND_STOP:
+      return "MEDIA_PLAYER_COMMAND_STOP";
+    case enums::MEDIA_PLAYER_COMMAND_MUTE:
+      return "MEDIA_PLAYER_COMMAND_MUTE";
+    case enums::MEDIA_PLAYER_COMMAND_UNMUTE:
+      return "MEDIA_PLAYER_COMMAND_UNMUTE";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string(enums::MediaPlayerFormatPurpose value) {
+  switch (value) {
+    case enums::MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT:
+      return "MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT";
+    case enums::MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT:
+      return "MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT";
+    default:
+      return "UNKNOWN";
+  }
+}
+#endif
+#ifdef USE_BLUETOOTH_PROXY
+template<>
+const char *proto_enum_to_string(enums::BluetoothDeviceRequestType value) {
+  switch (value) {
+    case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT:
+      return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT";
+    case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT:
+      return "BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT";
+    case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR:
+      return "BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR";
+    case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR:
+      return "BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR";
+    case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE:
+      return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE";
+    case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE:
+      return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE";
+    case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE:
+      return "BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string(enums::BluetoothScannerState value) {
+  switch (value) {
+    case enums::BLUETOOTH_SCANNER_STATE_IDLE:
+      return "BLUETOOTH_SCANNER_STATE_IDLE";
+    case enums::BLUETOOTH_SCANNER_STATE_STARTING:
+      return "BLUETOOTH_SCANNER_STATE_STARTING";
+    case enums::BLUETOOTH_SCANNER_STATE_RUNNING:
+      return "BLUETOOTH_SCANNER_STATE_RUNNING";
+    case enums::BLUETOOTH_SCANNER_STATE_FAILED:
+      return "BLUETOOTH_SCANNER_STATE_FAILED";
+    case enums::BLUETOOTH_SCANNER_STATE_STOPPING:
+      return "BLUETOOTH_SCANNER_STATE_STOPPING";
+    case enums::BLUETOOTH_SCANNER_STATE_STOPPED:
+      return "BLUETOOTH_SCANNER_STATE_STOPPED";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string(enums::BluetoothScannerMode value) {
+  switch (value) {
+    case enums::BLUETOOTH_SCANNER_MODE_PASSIVE:
+      return "BLUETOOTH_SCANNER_MODE_PASSIVE";
+    case enums::BLUETOOTH_SCANNER_MODE_ACTIVE:
+      return "BLUETOOTH_SCANNER_MODE_ACTIVE";
+    default:
+      return "UNKNOWN";
+  }
+}
+#endif
+template<>
+const char *proto_enum_to_string(enums::VoiceAssistantSubscribeFlag value) {
+  switch (value) {
+    case enums::VOICE_ASSISTANT_SUBSCRIBE_NONE:
+      return "VOICE_ASSISTANT_SUBSCRIBE_NONE";
+    case enums::VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO:
+      return "VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string(enums::VoiceAssistantRequestFlag value) {
+  switch (value) {
+    case enums::VOICE_ASSISTANT_REQUEST_NONE:
+      return "VOICE_ASSISTANT_REQUEST_NONE";
+    case enums::VOICE_ASSISTANT_REQUEST_USE_VAD:
+      return "VOICE_ASSISTANT_REQUEST_USE_VAD";
+    case enums::VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD:
+      return "VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD";
+    default:
+      return "UNKNOWN";
+  }
+}
+#ifdef USE_VOICE_ASSISTANT
+template<> const char *proto_enum_to_string(enums::VoiceAssistantEvent value) {
+  switch (value) {
+    case enums::VOICE_ASSISTANT_ERROR:
+      return "VOICE_ASSISTANT_ERROR";
+    case enums::VOICE_ASSISTANT_RUN_START:
+      return "VOICE_ASSISTANT_RUN_START";
+    case enums::VOICE_ASSISTANT_RUN_END:
+      return "VOICE_ASSISTANT_RUN_END";
+    case enums::VOICE_ASSISTANT_STT_START:
+      return "VOICE_ASSISTANT_STT_START";
+    case enums::VOICE_ASSISTANT_STT_END:
+      return "VOICE_ASSISTANT_STT_END";
+    case enums::VOICE_ASSISTANT_INTENT_START:
+      return "VOICE_ASSISTANT_INTENT_START";
+    case enums::VOICE_ASSISTANT_INTENT_END:
+      return "VOICE_ASSISTANT_INTENT_END";
+    case enums::VOICE_ASSISTANT_TTS_START:
+      return "VOICE_ASSISTANT_TTS_START";
+    case enums::VOICE_ASSISTANT_TTS_END:
+      return "VOICE_ASSISTANT_TTS_END";
+    case enums::VOICE_ASSISTANT_WAKE_WORD_START:
+      return "VOICE_ASSISTANT_WAKE_WORD_START";
+    case enums::VOICE_ASSISTANT_WAKE_WORD_END:
+      return "VOICE_ASSISTANT_WAKE_WORD_END";
+    case enums::VOICE_ASSISTANT_STT_VAD_START:
+      return "VOICE_ASSISTANT_STT_VAD_START";
+    case enums::VOICE_ASSISTANT_STT_VAD_END:
+      return "VOICE_ASSISTANT_STT_VAD_END";
+    case enums::VOICE_ASSISTANT_TTS_STREAM_START:
+      return "VOICE_ASSISTANT_TTS_STREAM_START";
+    case enums::VOICE_ASSISTANT_TTS_STREAM_END:
+      return "VOICE_ASSISTANT_TTS_STREAM_END";
+    case enums::VOICE_ASSISTANT_INTENT_PROGRESS:
+      return "VOICE_ASSISTANT_INTENT_PROGRESS";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string(enums::VoiceAssistantTimerEvent value) {
+  switch (value) {
+    case enums::VOICE_ASSISTANT_TIMER_STARTED:
+      return "VOICE_ASSISTANT_TIMER_STARTED";
+    case enums::VOICE_ASSISTANT_TIMER_UPDATED:
+      return "VOICE_ASSISTANT_TIMER_UPDATED";
+    case enums::VOICE_ASSISTANT_TIMER_CANCELLED:
+      return "VOICE_ASSISTANT_TIMER_CANCELLED";
+    case enums::VOICE_ASSISTANT_TIMER_FINISHED:
+      return "VOICE_ASSISTANT_TIMER_FINISHED";
+    default:
+      return "UNKNOWN";
+  }
+}
+#endif
+#ifdef USE_ALARM_CONTROL_PANEL
+template<> const char *proto_enum_to_string(enums::AlarmControlPanelState value) {
+  switch (value) {
+    case enums::ALARM_STATE_DISARMED:
+      return "ALARM_STATE_DISARMED";
+    case enums::ALARM_STATE_ARMED_HOME:
+      return "ALARM_STATE_ARMED_HOME";
+    case enums::ALARM_STATE_ARMED_AWAY:
+      return "ALARM_STATE_ARMED_AWAY";
+    case enums::ALARM_STATE_ARMED_NIGHT:
+      return "ALARM_STATE_ARMED_NIGHT";
+    case enums::ALARM_STATE_ARMED_VACATION:
+      return "ALARM_STATE_ARMED_VACATION";
+    case enums::ALARM_STATE_ARMED_CUSTOM_BYPASS:
+      return "ALARM_STATE_ARMED_CUSTOM_BYPASS";
+    case enums::ALARM_STATE_PENDING:
+      return "ALARM_STATE_PENDING";
+    case enums::ALARM_STATE_ARMING:
+      return "ALARM_STATE_ARMING";
+    case enums::ALARM_STATE_DISARMING:
+      return "ALARM_STATE_DISARMING";
+    case enums::ALARM_STATE_TRIGGERED:
+      return "ALARM_STATE_TRIGGERED";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<>
+const char *proto_enum_to_string(enums::AlarmControlPanelStateCommand value) {
+  switch (value) {
+    case enums::ALARM_CONTROL_PANEL_DISARM:
+      return "ALARM_CONTROL_PANEL_DISARM";
+    case enums::ALARM_CONTROL_PANEL_ARM_AWAY:
+      return "ALARM_CONTROL_PANEL_ARM_AWAY";
+    case enums::ALARM_CONTROL_PANEL_ARM_HOME:
+      return "ALARM_CONTROL_PANEL_ARM_HOME";
+    case enums::ALARM_CONTROL_PANEL_ARM_NIGHT:
+      return "ALARM_CONTROL_PANEL_ARM_NIGHT";
+    case enums::ALARM_CONTROL_PANEL_ARM_VACATION:
+      return "ALARM_CONTROL_PANEL_ARM_VACATION";
+    case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS:
+      return "ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS";
+    case enums::ALARM_CONTROL_PANEL_TRIGGER:
+      return "ALARM_CONTROL_PANEL_TRIGGER";
+    default:
+      return "UNKNOWN";
+  }
+}
+#endif
+#ifdef USE_TEXT
+template<> const char *proto_enum_to_string(enums::TextMode value) {
+  switch (value) {
+    case enums::TEXT_MODE_TEXT:
+      return "TEXT_MODE_TEXT";
+    case enums::TEXT_MODE_PASSWORD:
+      return "TEXT_MODE_PASSWORD";
+    default:
+      return "UNKNOWN";
+  }
+}
+#endif
+#ifdef USE_VALVE
+template<> const char *proto_enum_to_string(enums::ValveOperation value) {
+  switch (value) {
+    case enums::VALVE_OPERATION_IDLE:
+      return "VALVE_OPERATION_IDLE";
+    case enums::VALVE_OPERATION_IS_OPENING:
+      return "VALVE_OPERATION_IS_OPENING";
+    case enums::VALVE_OPERATION_IS_CLOSING:
+      return "VALVE_OPERATION_IS_CLOSING";
+    default:
+      return "UNKNOWN";
+  }
+}
+#endif
+#ifdef USE_UPDATE
+template<> const char *proto_enum_to_string(enums::UpdateCommand value) {
+  switch (value) {
+    case enums::UPDATE_COMMAND_NONE:
+      return "UPDATE_COMMAND_NONE";
+    case enums::UPDATE_COMMAND_UPDATE:
+      return "UPDATE_COMMAND_UPDATE";
+    case enums::UPDATE_COMMAND_CHECK:
+      return "UPDATE_COMMAND_CHECK";
+    default:
+      return "UNKNOWN";
+  }
+}
+#endif
+
+void HelloRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("HelloRequest {\n");
+  out.append("  client_info: ");
+  out.append("'").append(this->client_info).append("'");
+  out.append("\n");
+
+  out.append("  api_version_major: ");
+  sprintf(buffer, "%" PRIu32, this->api_version_major);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  api_version_minor: ");
+  sprintf(buffer, "%" PRIu32, this->api_version_minor);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void HelloResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("HelloResponse {\n");
+  out.append("  api_version_major: ");
+  sprintf(buffer, "%" PRIu32, this->api_version_major);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  api_version_minor: ");
+  sprintf(buffer, "%" PRIu32, this->api_version_minor);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  server_info: ");
+  out.append("'").append(this->server_info).append("'");
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+  out.append("}");
+}
+void ConnectRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ConnectRequest {\n");
+  out.append("  password: ");
+  out.append("'").append(this->password).append("'");
+  out.append("\n");
+  out.append("}");
+}
+void ConnectResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ConnectResponse {\n");
+  out.append("  invalid_password: ");
+  out.append(YESNO(this->invalid_password));
+  out.append("\n");
+  out.append("}");
+}
+void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); }
+void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); }
+void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); }
+void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {}"); }
+void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); }
+void AreaInfo::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("AreaInfo {\n");
+  out.append("  area_id: ");
+  sprintf(buffer, "%" PRIu32, this->area_id);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+  out.append("}");
+}
+void DeviceInfo::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("DeviceInfo {\n");
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  area_id: ");
+  sprintf(buffer, "%" PRIu32, this->area_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void DeviceInfoResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("DeviceInfoResponse {\n");
+  out.append("  uses_password: ");
+  out.append(YESNO(this->uses_password));
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  mac_address: ");
+  out.append("'").append(this->mac_address).append("'");
+  out.append("\n");
+
+  out.append("  esphome_version: ");
+  out.append("'").append(this->esphome_version).append("'");
+  out.append("\n");
+
+  out.append("  compilation_time: ");
+  out.append("'").append(this->compilation_time).append("'");
+  out.append("\n");
+
+  out.append("  model: ");
+  out.append("'").append(this->model).append("'");
+  out.append("\n");
+
+  out.append("  has_deep_sleep: ");
+  out.append(YESNO(this->has_deep_sleep));
+  out.append("\n");
+
+  out.append("  project_name: ");
+  out.append("'").append(this->project_name).append("'");
+  out.append("\n");
+
+  out.append("  project_version: ");
+  out.append("'").append(this->project_version).append("'");
+  out.append("\n");
+
+  out.append("  webserver_port: ");
+  sprintf(buffer, "%" PRIu32, this->webserver_port);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  legacy_bluetooth_proxy_version: ");
+  sprintf(buffer, "%" PRIu32, this->legacy_bluetooth_proxy_version);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  bluetooth_proxy_feature_flags: ");
+  sprintf(buffer, "%" PRIu32, this->bluetooth_proxy_feature_flags);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  manufacturer: ");
+  out.append("'").append(this->manufacturer).append("'");
+  out.append("\n");
+
+  out.append("  friendly_name: ");
+  out.append("'").append(this->friendly_name).append("'");
+  out.append("\n");
+
+  out.append("  legacy_voice_assistant_version: ");
+  sprintf(buffer, "%" PRIu32, this->legacy_voice_assistant_version);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  voice_assistant_feature_flags: ");
+  sprintf(buffer, "%" PRIu32, this->voice_assistant_feature_flags);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  suggested_area: ");
+  out.append("'").append(this->suggested_area).append("'");
+  out.append("\n");
+
+  out.append("  bluetooth_mac_address: ");
+  out.append("'").append(this->bluetooth_mac_address).append("'");
+  out.append("\n");
+
+  out.append("  api_encryption_supported: ");
+  out.append(YESNO(this->api_encryption_supported));
+  out.append("\n");
+
+  for (const auto &it : this->devices) {
+    out.append("  devices: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+
+  for (const auto &it : this->areas) {
+    out.append("  areas: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+
+  out.append("  area: ");
+  this->area.dump_to(out);
+  out.append("\n");
+  out.append("}");
+}
+void ListEntitiesRequest::dump_to(std::string &out) const { out.append("ListEntitiesRequest {}"); }
+void ListEntitiesDoneResponse::dump_to(std::string &out) const { out.append("ListEntitiesDoneResponse {}"); }
+void SubscribeStatesRequest::dump_to(std::string &out) const { out.append("SubscribeStatesRequest {}"); }
+#ifdef USE_BINARY_SENSOR
+void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesBinarySensorResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  device_class: ");
+  out.append("'").append(this->device_class).append("'");
+  out.append("\n");
+
+  out.append("  is_status_binary_sensor: ");
+  out.append(YESNO(this->is_status_binary_sensor));
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void BinarySensorStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BinarySensorStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+
+  out.append("  missing_state: ");
+  out.append(YESNO(this->missing_state));
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_COVER
+void ListEntitiesCoverResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesCoverResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  assumed_state: ");
+  out.append(YESNO(this->assumed_state));
+  out.append("\n");
+
+  out.append("  supports_position: ");
+  out.append(YESNO(this->supports_position));
+  out.append("\n");
+
+  out.append("  supports_tilt: ");
+  out.append(YESNO(this->supports_tilt));
+  out.append("\n");
+
+  out.append("  device_class: ");
+  out.append("'").append(this->device_class).append("'");
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  supports_stop: ");
+  out.append(YESNO(this->supports_stop));
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void CoverStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("CoverStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  legacy_state: ");
+  out.append(proto_enum_to_string(this->legacy_state));
+  out.append("\n");
+
+  out.append("  position: ");
+  sprintf(buffer, "%g", this->position);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  tilt: ");
+  sprintf(buffer, "%g", this->tilt);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  current_operation: ");
+  out.append(proto_enum_to_string(this->current_operation));
+  out.append("\n");
+  out.append("}");
+}
+void CoverCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("CoverCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_legacy_command: ");
+  out.append(YESNO(this->has_legacy_command));
+  out.append("\n");
+
+  out.append("  legacy_command: ");
+  out.append(proto_enum_to_string(this->legacy_command));
+  out.append("\n");
+
+  out.append("  has_position: ");
+  out.append(YESNO(this->has_position));
+  out.append("\n");
+
+  out.append("  position: ");
+  sprintf(buffer, "%g", this->position);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_tilt: ");
+  out.append(YESNO(this->has_tilt));
+  out.append("\n");
+
+  out.append("  tilt: ");
+  sprintf(buffer, "%g", this->tilt);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  stop: ");
+  out.append(YESNO(this->stop));
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_FAN
+void ListEntitiesFanResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesFanResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  supports_oscillation: ");
+  out.append(YESNO(this->supports_oscillation));
+  out.append("\n");
+
+  out.append("  supports_speed: ");
+  out.append(YESNO(this->supports_speed));
+  out.append("\n");
+
+  out.append("  supports_direction: ");
+  out.append(YESNO(this->supports_direction));
+  out.append("\n");
+
+  out.append("  supported_speed_count: ");
+  sprintf(buffer, "%" PRId32, this->supported_speed_count);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  for (const auto &it : this->supported_preset_modes) {
+    out.append("  supported_preset_modes: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void FanStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("FanStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+
+  out.append("  oscillating: ");
+  out.append(YESNO(this->oscillating));
+  out.append("\n");
+
+  out.append("  speed: ");
+  out.append(proto_enum_to_string(this->speed));
+  out.append("\n");
+
+  out.append("  direction: ");
+  out.append(proto_enum_to_string(this->direction));
+  out.append("\n");
+
+  out.append("  speed_level: ");
+  sprintf(buffer, "%" PRId32, this->speed_level);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  preset_mode: ");
+  out.append("'").append(this->preset_mode).append("'");
+  out.append("\n");
+  out.append("}");
+}
+void FanCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("FanCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_state: ");
+  out.append(YESNO(this->has_state));
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+
+  out.append("  has_speed: ");
+  out.append(YESNO(this->has_speed));
+  out.append("\n");
+
+  out.append("  speed: ");
+  out.append(proto_enum_to_string(this->speed));
+  out.append("\n");
+
+  out.append("  has_oscillating: ");
+  out.append(YESNO(this->has_oscillating));
+  out.append("\n");
+
+  out.append("  oscillating: ");
+  out.append(YESNO(this->oscillating));
+  out.append("\n");
+
+  out.append("  has_direction: ");
+  out.append(YESNO(this->has_direction));
+  out.append("\n");
+
+  out.append("  direction: ");
+  out.append(proto_enum_to_string(this->direction));
+  out.append("\n");
+
+  out.append("  has_speed_level: ");
+  out.append(YESNO(this->has_speed_level));
+  out.append("\n");
+
+  out.append("  speed_level: ");
+  sprintf(buffer, "%" PRId32, this->speed_level);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_preset_mode: ");
+  out.append(YESNO(this->has_preset_mode));
+  out.append("\n");
+
+  out.append("  preset_mode: ");
+  out.append("'").append(this->preset_mode).append("'");
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_LIGHT
+void ListEntitiesLightResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesLightResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  for (const auto &it : this->supported_color_modes) {
+    out.append("  supported_color_modes: ");
+    out.append(proto_enum_to_string(it));
+    out.append("\n");
+  }
+
+  out.append("  legacy_supports_brightness: ");
+  out.append(YESNO(this->legacy_supports_brightness));
+  out.append("\n");
+
+  out.append("  legacy_supports_rgb: ");
+  out.append(YESNO(this->legacy_supports_rgb));
+  out.append("\n");
+
+  out.append("  legacy_supports_white_value: ");
+  out.append(YESNO(this->legacy_supports_white_value));
+  out.append("\n");
+
+  out.append("  legacy_supports_color_temperature: ");
+  out.append(YESNO(this->legacy_supports_color_temperature));
+  out.append("\n");
+
+  out.append("  min_mireds: ");
+  sprintf(buffer, "%g", this->min_mireds);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  max_mireds: ");
+  sprintf(buffer, "%g", this->max_mireds);
+  out.append(buffer);
+  out.append("\n");
+
+  for (const auto &it : this->effects) {
+    out.append("  effects: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void LightStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("LightStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+
+  out.append("  brightness: ");
+  sprintf(buffer, "%g", this->brightness);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  color_mode: ");
+  out.append(proto_enum_to_string(this->color_mode));
+  out.append("\n");
+
+  out.append("  color_brightness: ");
+  sprintf(buffer, "%g", this->color_brightness);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  red: ");
+  sprintf(buffer, "%g", this->red);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  green: ");
+  sprintf(buffer, "%g", this->green);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  blue: ");
+  sprintf(buffer, "%g", this->blue);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  white: ");
+  sprintf(buffer, "%g", this->white);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  color_temperature: ");
+  sprintf(buffer, "%g", this->color_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  cold_white: ");
+  sprintf(buffer, "%g", this->cold_white);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  warm_white: ");
+  sprintf(buffer, "%g", this->warm_white);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  effect: ");
+  out.append("'").append(this->effect).append("'");
+  out.append("\n");
+  out.append("}");
+}
+void LightCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("LightCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_state: ");
+  out.append(YESNO(this->has_state));
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+
+  out.append("  has_brightness: ");
+  out.append(YESNO(this->has_brightness));
+  out.append("\n");
+
+  out.append("  brightness: ");
+  sprintf(buffer, "%g", this->brightness);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_color_mode: ");
+  out.append(YESNO(this->has_color_mode));
+  out.append("\n");
+
+  out.append("  color_mode: ");
+  out.append(proto_enum_to_string(this->color_mode));
+  out.append("\n");
+
+  out.append("  has_color_brightness: ");
+  out.append(YESNO(this->has_color_brightness));
+  out.append("\n");
+
+  out.append("  color_brightness: ");
+  sprintf(buffer, "%g", this->color_brightness);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_rgb: ");
+  out.append(YESNO(this->has_rgb));
+  out.append("\n");
+
+  out.append("  red: ");
+  sprintf(buffer, "%g", this->red);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  green: ");
+  sprintf(buffer, "%g", this->green);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  blue: ");
+  sprintf(buffer, "%g", this->blue);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_white: ");
+  out.append(YESNO(this->has_white));
+  out.append("\n");
+
+  out.append("  white: ");
+  sprintf(buffer, "%g", this->white);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_color_temperature: ");
+  out.append(YESNO(this->has_color_temperature));
+  out.append("\n");
+
+  out.append("  color_temperature: ");
+  sprintf(buffer, "%g", this->color_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_cold_white: ");
+  out.append(YESNO(this->has_cold_white));
+  out.append("\n");
+
+  out.append("  cold_white: ");
+  sprintf(buffer, "%g", this->cold_white);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_warm_white: ");
+  out.append(YESNO(this->has_warm_white));
+  out.append("\n");
+
+  out.append("  warm_white: ");
+  sprintf(buffer, "%g", this->warm_white);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_transition_length: ");
+  out.append(YESNO(this->has_transition_length));
+  out.append("\n");
+
+  out.append("  transition_length: ");
+  sprintf(buffer, "%" PRIu32, this->transition_length);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_flash_length: ");
+  out.append(YESNO(this->has_flash_length));
+  out.append("\n");
+
+  out.append("  flash_length: ");
+  sprintf(buffer, "%" PRIu32, this->flash_length);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_effect: ");
+  out.append(YESNO(this->has_effect));
+  out.append("\n");
+
+  out.append("  effect: ");
+  out.append("'").append(this->effect).append("'");
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_SENSOR
+void ListEntitiesSensorResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesSensorResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  unit_of_measurement: ");
+  out.append("'").append(this->unit_of_measurement).append("'");
+  out.append("\n");
+
+  out.append("  accuracy_decimals: ");
+  sprintf(buffer, "%" PRId32, this->accuracy_decimals);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  force_update: ");
+  out.append(YESNO(this->force_update));
+  out.append("\n");
+
+  out.append("  device_class: ");
+  out.append("'").append(this->device_class).append("'");
+  out.append("\n");
+
+  out.append("  state_class: ");
+  out.append(proto_enum_to_string(this->state_class));
+  out.append("\n");
+
+  out.append("  legacy_last_reset_type: ");
+  out.append(proto_enum_to_string(this->legacy_last_reset_type));
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void SensorStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("SensorStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  sprintf(buffer, "%g", this->state);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  missing_state: ");
+  out.append(YESNO(this->missing_state));
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_SWITCH
+void ListEntitiesSwitchResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesSwitchResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  assumed_state: ");
+  out.append(YESNO(this->assumed_state));
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  device_class: ");
+  out.append("'").append(this->device_class).append("'");
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void SwitchStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("SwitchStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+  out.append("}");
+}
+void SwitchCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("SwitchCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_TEXT_SENSOR
+void ListEntitiesTextSensorResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesTextSensorResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  device_class: ");
+  out.append("'").append(this->device_class).append("'");
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void TextSensorStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("TextSensorStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append("'").append(this->state).append("'");
+  out.append("\n");
+
+  out.append("  missing_state: ");
+  out.append(YESNO(this->missing_state));
+  out.append("\n");
+  out.append("}");
+}
+#endif
+void SubscribeLogsRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("SubscribeLogsRequest {\n");
+  out.append("  level: ");
+  out.append(proto_enum_to_string(this->level));
+  out.append("\n");
+
+  out.append("  dump_config: ");
+  out.append(YESNO(this->dump_config));
+  out.append("\n");
+  out.append("}");
+}
+void SubscribeLogsResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("SubscribeLogsResponse {\n");
+  out.append("  level: ");
+  out.append(proto_enum_to_string(this->level));
+  out.append("\n");
+
+  out.append("  message: ");
+  out.append(format_hex_pretty(this->message));
+  out.append("\n");
+
+  out.append("  send_failed: ");
+  out.append(YESNO(this->send_failed));
+  out.append("\n");
+  out.append("}");
+}
+#ifdef USE_API_NOISE
+void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("NoiseEncryptionSetKeyRequest {\n");
+  out.append("  key: ");
+  out.append(format_hex_pretty(this->key));
+  out.append("\n");
+  out.append("}");
+}
+void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("NoiseEncryptionSetKeyResponse {\n");
+  out.append("  success: ");
+  out.append(YESNO(this->success));
+  out.append("\n");
+  out.append("}");
+}
+#endif
+void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const {
+  out.append("SubscribeHomeassistantServicesRequest {}");
+}
+void HomeassistantServiceMap::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("HomeassistantServiceMap {\n");
+  out.append("  key: ");
+  out.append("'").append(this->key).append("'");
+  out.append("\n");
+
+  out.append("  value: ");
+  out.append("'").append(this->value).append("'");
+  out.append("\n");
+  out.append("}");
+}
+void HomeassistantServiceResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("HomeassistantServiceResponse {\n");
+  out.append("  service: ");
+  out.append("'").append(this->service).append("'");
+  out.append("\n");
+
+  for (const auto &it : this->data) {
+    out.append("  data: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+
+  for (const auto &it : this->data_template) {
+    out.append("  data_template: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+
+  for (const auto &it : this->variables) {
+    out.append("  variables: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+
+  out.append("  is_event: ");
+  out.append(YESNO(this->is_event));
+  out.append("\n");
+  out.append("}");
+}
+void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const {
+  out.append("SubscribeHomeAssistantStatesRequest {}");
+}
+void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("SubscribeHomeAssistantStateResponse {\n");
+  out.append("  entity_id: ");
+  out.append("'").append(this->entity_id).append("'");
+  out.append("\n");
+
+  out.append("  attribute: ");
+  out.append("'").append(this->attribute).append("'");
+  out.append("\n");
+
+  out.append("  once: ");
+  out.append(YESNO(this->once));
+  out.append("\n");
+  out.append("}");
+}
+void HomeAssistantStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("HomeAssistantStateResponse {\n");
+  out.append("  entity_id: ");
+  out.append("'").append(this->entity_id).append("'");
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append("'").append(this->state).append("'");
+  out.append("\n");
+
+  out.append("  attribute: ");
+  out.append("'").append(this->attribute).append("'");
+  out.append("\n");
+  out.append("}");
+}
+void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); }
+void GetTimeResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("GetTimeResponse {\n");
+  out.append("  epoch_seconds: ");
+  sprintf(buffer, "%" PRIu32, this->epoch_seconds);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void ListEntitiesServicesArgument::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesServicesArgument {\n");
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  type: ");
+  out.append(proto_enum_to_string(this->type));
+  out.append("\n");
+  out.append("}");
+}
+void ListEntitiesServicesResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesServicesResponse {\n");
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  for (const auto &it : this->args) {
+    out.append("  args: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+  out.append("}");
+}
+void ExecuteServiceArgument::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ExecuteServiceArgument {\n");
+  out.append("  bool_: ");
+  out.append(YESNO(this->bool_));
+  out.append("\n");
+
+  out.append("  legacy_int: ");
+  sprintf(buffer, "%" PRId32, this->legacy_int);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  float_: ");
+  sprintf(buffer, "%g", this->float_);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  string_: ");
+  out.append("'").append(this->string_).append("'");
+  out.append("\n");
+
+  out.append("  int_: ");
+  sprintf(buffer, "%" PRId32, this->int_);
+  out.append(buffer);
+  out.append("\n");
+
+  for (const auto it : this->bool_array) {
+    out.append("  bool_array: ");
+    out.append(YESNO(it));
+    out.append("\n");
+  }
+
+  for (const auto &it : this->int_array) {
+    out.append("  int_array: ");
+    sprintf(buffer, "%" PRId32, it);
+    out.append(buffer);
+    out.append("\n");
+  }
+
+  for (const auto &it : this->float_array) {
+    out.append("  float_array: ");
+    sprintf(buffer, "%g", it);
+    out.append(buffer);
+    out.append("\n");
+  }
+
+  for (const auto &it : this->string_array) {
+    out.append("  string_array: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
+  out.append("}");
+}
+void ExecuteServiceRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ExecuteServiceRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  for (const auto &it : this->args) {
+    out.append("  args: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+  out.append("}");
+}
+#ifdef USE_ESP32_CAMERA
+void ListEntitiesCameraResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesCameraResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void CameraImageResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("CameraImageResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  data: ");
+  out.append(format_hex_pretty(this->data));
+  out.append("\n");
+
+  out.append("  done: ");
+  out.append(YESNO(this->done));
+  out.append("\n");
+  out.append("}");
+}
+void CameraImageRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("CameraImageRequest {\n");
+  out.append("  single: ");
+  out.append(YESNO(this->single));
+  out.append("\n");
+
+  out.append("  stream: ");
+  out.append(YESNO(this->stream));
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_CLIMATE
+void ListEntitiesClimateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesClimateResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  supports_current_temperature: ");
+  out.append(YESNO(this->supports_current_temperature));
+  out.append("\n");
+
+  out.append("  supports_two_point_target_temperature: ");
+  out.append(YESNO(this->supports_two_point_target_temperature));
+  out.append("\n");
+
+  for (const auto &it : this->supported_modes) {
+    out.append("  supported_modes: ");
+    out.append(proto_enum_to_string(it));
+    out.append("\n");
+  }
+
+  out.append("  visual_min_temperature: ");
+  sprintf(buffer, "%g", this->visual_min_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  visual_max_temperature: ");
+  sprintf(buffer, "%g", this->visual_max_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  visual_target_temperature_step: ");
+  sprintf(buffer, "%g", this->visual_target_temperature_step);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  legacy_supports_away: ");
+  out.append(YESNO(this->legacy_supports_away));
+  out.append("\n");
+
+  out.append("  supports_action: ");
+  out.append(YESNO(this->supports_action));
+  out.append("\n");
+
+  for (const auto &it : this->supported_fan_modes) {
+    out.append("  supported_fan_modes: ");
+    out.append(proto_enum_to_string(it));
+    out.append("\n");
+  }
+
+  for (const auto &it : this->supported_swing_modes) {
+    out.append("  supported_swing_modes: ");
+    out.append(proto_enum_to_string(it));
+    out.append("\n");
+  }
+
+  for (const auto &it : this->supported_custom_fan_modes) {
+    out.append("  supported_custom_fan_modes: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
+
+  for (const auto &it : this->supported_presets) {
+    out.append("  supported_presets: ");
+    out.append(proto_enum_to_string(it));
+    out.append("\n");
+  }
+
+  for (const auto &it : this->supported_custom_presets) {
+    out.append("  supported_custom_presets: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  visual_current_temperature_step: ");
+  sprintf(buffer, "%g", this->visual_current_temperature_step);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  supports_current_humidity: ");
+  out.append(YESNO(this->supports_current_humidity));
+  out.append("\n");
+
+  out.append("  supports_target_humidity: ");
+  out.append(YESNO(this->supports_target_humidity));
+  out.append("\n");
+
+  out.append("  visual_min_humidity: ");
+  sprintf(buffer, "%g", this->visual_min_humidity);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  visual_max_humidity: ");
+  sprintf(buffer, "%g", this->visual_max_humidity);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void ClimateStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ClimateStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  mode: ");
+  out.append(proto_enum_to_string(this->mode));
+  out.append("\n");
+
+  out.append("  current_temperature: ");
+  sprintf(buffer, "%g", this->current_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  target_temperature: ");
+  sprintf(buffer, "%g", this->target_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  target_temperature_low: ");
+  sprintf(buffer, "%g", this->target_temperature_low);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  target_temperature_high: ");
+  sprintf(buffer, "%g", this->target_temperature_high);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  unused_legacy_away: ");
+  out.append(YESNO(this->unused_legacy_away));
+  out.append("\n");
+
+  out.append("  action: ");
+  out.append(proto_enum_to_string(this->action));
+  out.append("\n");
+
+  out.append("  fan_mode: ");
+  out.append(proto_enum_to_string(this->fan_mode));
+  out.append("\n");
+
+  out.append("  swing_mode: ");
+  out.append(proto_enum_to_string(this->swing_mode));
+  out.append("\n");
+
+  out.append("  custom_fan_mode: ");
+  out.append("'").append(this->custom_fan_mode).append("'");
+  out.append("\n");
+
+  out.append("  preset: ");
+  out.append(proto_enum_to_string(this->preset));
+  out.append("\n");
+
+  out.append("  custom_preset: ");
+  out.append("'").append(this->custom_preset).append("'");
+  out.append("\n");
+
+  out.append("  current_humidity: ");
+  sprintf(buffer, "%g", this->current_humidity);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  target_humidity: ");
+  sprintf(buffer, "%g", this->target_humidity);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void ClimateCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ClimateCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_mode: ");
+  out.append(YESNO(this->has_mode));
+  out.append("\n");
+
+  out.append("  mode: ");
+  out.append(proto_enum_to_string(this->mode));
+  out.append("\n");
+
+  out.append("  has_target_temperature: ");
+  out.append(YESNO(this->has_target_temperature));
+  out.append("\n");
+
+  out.append("  target_temperature: ");
+  sprintf(buffer, "%g", this->target_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_target_temperature_low: ");
+  out.append(YESNO(this->has_target_temperature_low));
+  out.append("\n");
+
+  out.append("  target_temperature_low: ");
+  sprintf(buffer, "%g", this->target_temperature_low);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_target_temperature_high: ");
+  out.append(YESNO(this->has_target_temperature_high));
+  out.append("\n");
+
+  out.append("  target_temperature_high: ");
+  sprintf(buffer, "%g", this->target_temperature_high);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  unused_has_legacy_away: ");
+  out.append(YESNO(this->unused_has_legacy_away));
+  out.append("\n");
+
+  out.append("  unused_legacy_away: ");
+  out.append(YESNO(this->unused_legacy_away));
+  out.append("\n");
+
+  out.append("  has_fan_mode: ");
+  out.append(YESNO(this->has_fan_mode));
+  out.append("\n");
+
+  out.append("  fan_mode: ");
+  out.append(proto_enum_to_string(this->fan_mode));
+  out.append("\n");
+
+  out.append("  has_swing_mode: ");
+  out.append(YESNO(this->has_swing_mode));
+  out.append("\n");
+
+  out.append("  swing_mode: ");
+  out.append(proto_enum_to_string(this->swing_mode));
+  out.append("\n");
+
+  out.append("  has_custom_fan_mode: ");
+  out.append(YESNO(this->has_custom_fan_mode));
+  out.append("\n");
+
+  out.append("  custom_fan_mode: ");
+  out.append("'").append(this->custom_fan_mode).append("'");
+  out.append("\n");
+
+  out.append("  has_preset: ");
+  out.append(YESNO(this->has_preset));
+  out.append("\n");
+
+  out.append("  preset: ");
+  out.append(proto_enum_to_string(this->preset));
+  out.append("\n");
+
+  out.append("  has_custom_preset: ");
+  out.append(YESNO(this->has_custom_preset));
+  out.append("\n");
+
+  out.append("  custom_preset: ");
+  out.append("'").append(this->custom_preset).append("'");
+  out.append("\n");
+
+  out.append("  has_target_humidity: ");
+  out.append(YESNO(this->has_target_humidity));
+  out.append("\n");
+
+  out.append("  target_humidity: ");
+  sprintf(buffer, "%g", this->target_humidity);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_NUMBER
+void ListEntitiesNumberResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesNumberResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  min_value: ");
+  sprintf(buffer, "%g", this->min_value);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  max_value: ");
+  sprintf(buffer, "%g", this->max_value);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  step: ");
+  sprintf(buffer, "%g", this->step);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  unit_of_measurement: ");
+  out.append("'").append(this->unit_of_measurement).append("'");
+  out.append("\n");
+
+  out.append("  mode: ");
+  out.append(proto_enum_to_string(this->mode));
+  out.append("\n");
+
+  out.append("  device_class: ");
+  out.append("'").append(this->device_class).append("'");
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void NumberStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("NumberStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  sprintf(buffer, "%g", this->state);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  missing_state: ");
+  out.append(YESNO(this->missing_state));
+  out.append("\n");
+  out.append("}");
+}
+void NumberCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("NumberCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  sprintf(buffer, "%g", this->state);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_SELECT
+void ListEntitiesSelectResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesSelectResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  for (const auto &it : this->options) {
+    out.append("  options: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void SelectStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("SelectStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append("'").append(this->state).append("'");
+  out.append("\n");
+
+  out.append("  missing_state: ");
+  out.append(YESNO(this->missing_state));
+  out.append("\n");
+  out.append("}");
+}
+void SelectCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("SelectCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append("'").append(this->state).append("'");
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_SIREN
+void ListEntitiesSirenResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesSirenResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  for (const auto &it : this->tones) {
+    out.append("  tones: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
+
+  out.append("  supports_duration: ");
+  out.append(YESNO(this->supports_duration));
+  out.append("\n");
+
+  out.append("  supports_volume: ");
+  out.append(YESNO(this->supports_volume));
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void SirenStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("SirenStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+  out.append("}");
+}
+void SirenCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("SirenCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_state: ");
+  out.append(YESNO(this->has_state));
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+
+  out.append("  has_tone: ");
+  out.append(YESNO(this->has_tone));
+  out.append("\n");
+
+  out.append("  tone: ");
+  out.append("'").append(this->tone).append("'");
+  out.append("\n");
+
+  out.append("  has_duration: ");
+  out.append(YESNO(this->has_duration));
+  out.append("\n");
+
+  out.append("  duration: ");
+  sprintf(buffer, "%" PRIu32, this->duration);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_volume: ");
+  out.append(YESNO(this->has_volume));
+  out.append("\n");
+
+  out.append("  volume: ");
+  sprintf(buffer, "%g", this->volume);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_LOCK
+void ListEntitiesLockResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesLockResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  assumed_state: ");
+  out.append(YESNO(this->assumed_state));
+  out.append("\n");
+
+  out.append("  supports_open: ");
+  out.append(YESNO(this->supports_open));
+  out.append("\n");
+
+  out.append("  requires_code: ");
+  out.append(YESNO(this->requires_code));
+  out.append("\n");
+
+  out.append("  code_format: ");
+  out.append("'").append(this->code_format).append("'");
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void LockStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("LockStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(proto_enum_to_string(this->state));
+  out.append("\n");
+  out.append("}");
+}
+void LockCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("LockCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  command: ");
+  out.append(proto_enum_to_string(this->command));
+  out.append("\n");
+
+  out.append("  has_code: ");
+  out.append(YESNO(this->has_code));
+  out.append("\n");
+
+  out.append("  code: ");
+  out.append("'").append(this->code).append("'");
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_BUTTON
+void ListEntitiesButtonResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesButtonResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  device_class: ");
+  out.append("'").append(this->device_class).append("'");
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void ButtonCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ButtonCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_MEDIA_PLAYER
+void MediaPlayerSupportedFormat::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("MediaPlayerSupportedFormat {\n");
+  out.append("  format: ");
+  out.append("'").append(this->format).append("'");
+  out.append("\n");
+
+  out.append("  sample_rate: ");
+  sprintf(buffer, "%" PRIu32, this->sample_rate);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  num_channels: ");
+  sprintf(buffer, "%" PRIu32, this->num_channels);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  purpose: ");
+  out.append(proto_enum_to_string(this->purpose));
+  out.append("\n");
+
+  out.append("  sample_bytes: ");
+  sprintf(buffer, "%" PRIu32, this->sample_bytes);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesMediaPlayerResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  supports_pause: ");
+  out.append(YESNO(this->supports_pause));
+  out.append("\n");
+
+  for (const auto &it : this->supported_formats) {
+    out.append("  supported_formats: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void MediaPlayerStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("MediaPlayerStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(proto_enum_to_string(this->state));
+  out.append("\n");
+
+  out.append("  volume: ");
+  sprintf(buffer, "%g", this->volume);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  muted: ");
+  out.append(YESNO(this->muted));
+  out.append("\n");
+  out.append("}");
+}
+void MediaPlayerCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("MediaPlayerCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_command: ");
+  out.append(YESNO(this->has_command));
+  out.append("\n");
+
+  out.append("  command: ");
+  out.append(proto_enum_to_string(this->command));
+  out.append("\n");
+
+  out.append("  has_volume: ");
+  out.append(YESNO(this->has_volume));
+  out.append("\n");
+
+  out.append("  volume: ");
+  sprintf(buffer, "%g", this->volume);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_media_url: ");
+  out.append(YESNO(this->has_media_url));
+  out.append("\n");
+
+  out.append("  media_url: ");
+  out.append("'").append(this->media_url).append("'");
+  out.append("\n");
+
+  out.append("  has_announcement: ");
+  out.append(YESNO(this->has_announcement));
+  out.append("\n");
+
+  out.append("  announcement: ");
+  out.append(YESNO(this->announcement));
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_BLUETOOTH_PROXY
+void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("SubscribeBluetoothLEAdvertisementsRequest {\n");
+  out.append("  flags: ");
+  sprintf(buffer, "%" PRIu32, this->flags);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothServiceData::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothServiceData {\n");
+  out.append("  uuid: ");
+  out.append("'").append(this->uuid).append("'");
+  out.append("\n");
+
+  for (const auto &it : this->legacy_data) {
+    out.append("  legacy_data: ");
+    sprintf(buffer, "%" PRIu32, it);
+    out.append(buffer);
+    out.append("\n");
+  }
+
+  out.append("  data: ");
+  out.append(format_hex_pretty(this->data));
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothLEAdvertisementResponse {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append(format_hex_pretty(this->name));
+  out.append("\n");
+
+  out.append("  rssi: ");
+  sprintf(buffer, "%" PRId32, this->rssi);
+  out.append(buffer);
+  out.append("\n");
+
+  for (const auto &it : this->service_uuids) {
+    out.append("  service_uuids: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
+
+  for (const auto &it : this->service_data) {
+    out.append("  service_data: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+
+  for (const auto &it : this->manufacturer_data) {
+    out.append("  manufacturer_data: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+
+  out.append("  address_type: ");
+  sprintf(buffer, "%" PRIu32, this->address_type);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothLERawAdvertisement::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothLERawAdvertisement {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  rssi: ");
+  sprintf(buffer, "%" PRId32, this->rssi);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  address_type: ");
+  sprintf(buffer, "%" PRIu32, this->address_type);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  data: ");
+  out.append(format_hex_pretty(this->data));
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothLERawAdvertisementsResponse {\n");
+  for (const auto &it : this->advertisements) {
+    out.append("  advertisements: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+  out.append("}");
+}
+void BluetoothDeviceRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothDeviceRequest {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  request_type: ");
+  out.append(proto_enum_to_string(this->request_type));
+  out.append("\n");
+
+  out.append("  has_address_type: ");
+  out.append(YESNO(this->has_address_type));
+  out.append("\n");
+
+  out.append("  address_type: ");
+  sprintf(buffer, "%" PRIu32, this->address_type);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothDeviceConnectionResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothDeviceConnectionResponse {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  connected: ");
+  out.append(YESNO(this->connected));
+  out.append("\n");
+
+  out.append("  mtu: ");
+  sprintf(buffer, "%" PRIu32, this->mtu);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  error: ");
+  sprintf(buffer, "%" PRId32, this->error);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothGATTGetServicesRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothGATTGetServicesRequest {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothGATTDescriptor::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothGATTDescriptor {\n");
+  for (const auto &it : this->uuid) {
+    out.append("  uuid: ");
+    sprintf(buffer, "%llu", it);
+    out.append(buffer);
+    out.append("\n");
+  }
+
+  out.append("  handle: ");
+  sprintf(buffer, "%" PRIu32, this->handle);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothGATTCharacteristic::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothGATTCharacteristic {\n");
+  for (const auto &it : this->uuid) {
+    out.append("  uuid: ");
+    sprintf(buffer, "%llu", it);
+    out.append(buffer);
+    out.append("\n");
+  }
+
+  out.append("  handle: ");
+  sprintf(buffer, "%" PRIu32, this->handle);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  properties: ");
+  sprintf(buffer, "%" PRIu32, this->properties);
+  out.append(buffer);
+  out.append("\n");
+
+  for (const auto &it : this->descriptors) {
+    out.append("  descriptors: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+  out.append("}");
+}
+void BluetoothGATTService::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothGATTService {\n");
+  for (const auto &it : this->uuid) {
+    out.append("  uuid: ");
+    sprintf(buffer, "%llu", it);
+    out.append(buffer);
+    out.append("\n");
+  }
+
+  out.append("  handle: ");
+  sprintf(buffer, "%" PRIu32, this->handle);
+  out.append(buffer);
+  out.append("\n");
+
+  for (const auto &it : this->characteristics) {
+    out.append("  characteristics: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+  out.append("}");
+}
+void BluetoothGATTGetServicesResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothGATTGetServicesResponse {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+
+  for (const auto &it : this->services) {
+    out.append("  services: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+  out.append("}");
+}
+void BluetoothGATTGetServicesDoneResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothGATTGetServicesDoneResponse {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothGATTReadRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothGATTReadRequest {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  handle: ");
+  sprintf(buffer, "%" PRIu32, this->handle);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothGATTReadResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothGATTReadResponse {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  handle: ");
+  sprintf(buffer, "%" PRIu32, this->handle);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  data: ");
+  out.append(format_hex_pretty(this->data));
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothGATTWriteRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothGATTWriteRequest {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  handle: ");
+  sprintf(buffer, "%" PRIu32, this->handle);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  response: ");
+  out.append(YESNO(this->response));
+  out.append("\n");
+
+  out.append("  data: ");
+  out.append(format_hex_pretty(this->data));
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothGATTReadDescriptorRequest {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  handle: ");
+  sprintf(buffer, "%" PRIu32, this->handle);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothGATTWriteDescriptorRequest {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  handle: ");
+  sprintf(buffer, "%" PRIu32, this->handle);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  data: ");
+  out.append(format_hex_pretty(this->data));
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothGATTNotifyRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothGATTNotifyRequest {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  handle: ");
+  sprintf(buffer, "%" PRIu32, this->handle);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  enable: ");
+  out.append(YESNO(this->enable));
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothGATTNotifyDataResponse {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  handle: ");
+  sprintf(buffer, "%" PRIu32, this->handle);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  data: ");
+  out.append(format_hex_pretty(this->data));
+  out.append("\n");
+  out.append("}");
+}
+void SubscribeBluetoothConnectionsFreeRequest::dump_to(std::string &out) const {
+  out.append("SubscribeBluetoothConnectionsFreeRequest {}");
+}
+void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothConnectionsFreeResponse {\n");
+  out.append("  free: ");
+  sprintf(buffer, "%" PRIu32, this->free);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  limit: ");
+  sprintf(buffer, "%" PRIu32, this->limit);
+  out.append(buffer);
+  out.append("\n");
+
+  for (const auto &it : this->allocated) {
+    out.append("  allocated: ");
+    sprintf(buffer, "%llu", it);
+    out.append(buffer);
+    out.append("\n");
+  }
+  out.append("}");
+}
+void BluetoothGATTErrorResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothGATTErrorResponse {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  handle: ");
+  sprintf(buffer, "%" PRIu32, this->handle);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  error: ");
+  sprintf(buffer, "%" PRId32, this->error);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothGATTWriteResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothGATTWriteResponse {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  handle: ");
+  sprintf(buffer, "%" PRIu32, this->handle);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothGATTNotifyResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothGATTNotifyResponse {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  handle: ");
+  sprintf(buffer, "%" PRIu32, this->handle);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothDevicePairingResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothDevicePairingResponse {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  paired: ");
+  out.append(YESNO(this->paired));
+  out.append("\n");
+
+  out.append("  error: ");
+  sprintf(buffer, "%" PRId32, this->error);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothDeviceUnpairingResponse {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  success: ");
+  out.append(YESNO(this->success));
+  out.append("\n");
+
+  out.append("  error: ");
+  sprintf(buffer, "%" PRId32, this->error);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void UnsubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const {
+  out.append("UnsubscribeBluetoothLEAdvertisementsRequest {}");
+}
+void BluetoothDeviceClearCacheResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothDeviceClearCacheResponse {\n");
+  out.append("  address: ");
+  sprintf(buffer, "%llu", this->address);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  success: ");
+  out.append(YESNO(this->success));
+  out.append("\n");
+
+  out.append("  error: ");
+  sprintf(buffer, "%" PRId32, this->error);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothScannerStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothScannerStateResponse {\n");
+  out.append("  state: ");
+  out.append(proto_enum_to_string(this->state));
+  out.append("\n");
+
+  out.append("  mode: ");
+  out.append(proto_enum_to_string(this->mode));
+  out.append("\n");
+  out.append("}");
+}
+void BluetoothScannerSetModeRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("BluetoothScannerSetModeRequest {\n");
+  out.append("  mode: ");
+  out.append(proto_enum_to_string(this->mode));
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_VOICE_ASSISTANT
+void SubscribeVoiceAssistantRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("SubscribeVoiceAssistantRequest {\n");
+  out.append("  subscribe: ");
+  out.append(YESNO(this->subscribe));
+  out.append("\n");
+
+  out.append("  flags: ");
+  sprintf(buffer, "%" PRIu32, this->flags);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void VoiceAssistantAudioSettings::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("VoiceAssistantAudioSettings {\n");
+  out.append("  noise_suppression_level: ");
+  sprintf(buffer, "%" PRIu32, this->noise_suppression_level);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  auto_gain: ");
+  sprintf(buffer, "%" PRIu32, this->auto_gain);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  volume_multiplier: ");
+  sprintf(buffer, "%g", this->volume_multiplier);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void VoiceAssistantRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("VoiceAssistantRequest {\n");
+  out.append("  start: ");
+  out.append(YESNO(this->start));
+  out.append("\n");
+
+  out.append("  conversation_id: ");
+  out.append("'").append(this->conversation_id).append("'");
+  out.append("\n");
+
+  out.append("  flags: ");
+  sprintf(buffer, "%" PRIu32, this->flags);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  audio_settings: ");
+  this->audio_settings.dump_to(out);
+  out.append("\n");
+
+  out.append("  wake_word_phrase: ");
+  out.append("'").append(this->wake_word_phrase).append("'");
+  out.append("\n");
+  out.append("}");
+}
+void VoiceAssistantResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("VoiceAssistantResponse {\n");
+  out.append("  port: ");
+  sprintf(buffer, "%" PRIu32, this->port);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  error: ");
+  out.append(YESNO(this->error));
+  out.append("\n");
+  out.append("}");
+}
+void VoiceAssistantEventData::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("VoiceAssistantEventData {\n");
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  value: ");
+  out.append("'").append(this->value).append("'");
+  out.append("\n");
+  out.append("}");
+}
+void VoiceAssistantEventResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("VoiceAssistantEventResponse {\n");
+  out.append("  event_type: ");
+  out.append(proto_enum_to_string(this->event_type));
+  out.append("\n");
+
+  for (const auto &it : this->data) {
+    out.append("  data: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+  out.append("}");
+}
+void VoiceAssistantAudio::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("VoiceAssistantAudio {\n");
+  out.append("  data: ");
+  out.append(format_hex_pretty(this->data));
+  out.append("\n");
+
+  out.append("  end: ");
+  out.append(YESNO(this->end));
+  out.append("\n");
+  out.append("}");
+}
+void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("VoiceAssistantTimerEventResponse {\n");
+  out.append("  event_type: ");
+  out.append(proto_enum_to_string(this->event_type));
+  out.append("\n");
+
+  out.append("  timer_id: ");
+  out.append("'").append(this->timer_id).append("'");
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  total_seconds: ");
+  sprintf(buffer, "%" PRIu32, this->total_seconds);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  seconds_left: ");
+  sprintf(buffer, "%" PRIu32, this->seconds_left);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  is_active: ");
+  out.append(YESNO(this->is_active));
+  out.append("\n");
+  out.append("}");
+}
+void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("VoiceAssistantAnnounceRequest {\n");
+  out.append("  media_id: ");
+  out.append("'").append(this->media_id).append("'");
+  out.append("\n");
+
+  out.append("  text: ");
+  out.append("'").append(this->text).append("'");
+  out.append("\n");
+
+  out.append("  preannounce_media_id: ");
+  out.append("'").append(this->preannounce_media_id).append("'");
+  out.append("\n");
+
+  out.append("  start_conversation: ");
+  out.append(YESNO(this->start_conversation));
+  out.append("\n");
+  out.append("}");
+}
+void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("VoiceAssistantAnnounceFinished {\n");
+  out.append("  success: ");
+  out.append(YESNO(this->success));
+  out.append("\n");
+  out.append("}");
+}
+void VoiceAssistantWakeWord::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("VoiceAssistantWakeWord {\n");
+  out.append("  id: ");
+  out.append("'").append(this->id).append("'");
+  out.append("\n");
+
+  out.append("  wake_word: ");
+  out.append("'").append(this->wake_word).append("'");
+  out.append("\n");
+
+  for (const auto &it : this->trained_languages) {
+    out.append("  trained_languages: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
+  out.append("}");
+}
+void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const {
+  out.append("VoiceAssistantConfigurationRequest {}");
+}
+void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("VoiceAssistantConfigurationResponse {\n");
+  for (const auto &it : this->available_wake_words) {
+    out.append("  available_wake_words: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+
+  for (const auto &it : this->active_wake_words) {
+    out.append("  active_wake_words: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
+
+  out.append("  max_active_wake_words: ");
+  sprintf(buffer, "%" PRIu32, this->max_active_wake_words);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void VoiceAssistantSetConfiguration::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("VoiceAssistantSetConfiguration {\n");
+  for (const auto &it : this->active_wake_words) {
+    out.append("  active_wake_words: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
+  out.append("}");
+}
+#endif
+#ifdef USE_ALARM_CONTROL_PANEL
+void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesAlarmControlPanelResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  supported_features: ");
+  sprintf(buffer, "%" PRIu32, this->supported_features);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  requires_code: ");
+  out.append(YESNO(this->requires_code));
+  out.append("\n");
+
+  out.append("  requires_code_to_arm: ");
+  out.append(YESNO(this->requires_code_to_arm));
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void AlarmControlPanelStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("AlarmControlPanelStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(proto_enum_to_string(this->state));
+  out.append("\n");
+  out.append("}");
+}
+void AlarmControlPanelCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("AlarmControlPanelCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  command: ");
+  out.append(proto_enum_to_string(this->command));
+  out.append("\n");
+
+  out.append("  code: ");
+  out.append("'").append(this->code).append("'");
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_TEXT
+void ListEntitiesTextResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesTextResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  min_length: ");
+  sprintf(buffer, "%" PRIu32, this->min_length);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  max_length: ");
+  sprintf(buffer, "%" PRIu32, this->max_length);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  pattern: ");
+  out.append("'").append(this->pattern).append("'");
+  out.append("\n");
+
+  out.append("  mode: ");
+  out.append(proto_enum_to_string(this->mode));
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void TextStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("TextStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append("'").append(this->state).append("'");
+  out.append("\n");
+
+  out.append("  missing_state: ");
+  out.append(YESNO(this->missing_state));
+  out.append("\n");
+  out.append("}");
+}
+void TextCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("TextCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append("'").append(this->state).append("'");
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_DATETIME_DATE
+void ListEntitiesDateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesDateResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void DateStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("DateStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  missing_state: ");
+  out.append(YESNO(this->missing_state));
+  out.append("\n");
+
+  out.append("  year: ");
+  sprintf(buffer, "%" PRIu32, this->year);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  month: ");
+  sprintf(buffer, "%" PRIu32, this->month);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  day: ");
+  sprintf(buffer, "%" PRIu32, this->day);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void DateCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("DateCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  year: ");
+  sprintf(buffer, "%" PRIu32, this->year);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  month: ");
+  sprintf(buffer, "%" PRIu32, this->month);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  day: ");
+  sprintf(buffer, "%" PRIu32, this->day);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_DATETIME_TIME
+void ListEntitiesTimeResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesTimeResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void TimeStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("TimeStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  missing_state: ");
+  out.append(YESNO(this->missing_state));
+  out.append("\n");
+
+  out.append("  hour: ");
+  sprintf(buffer, "%" PRIu32, this->hour);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  minute: ");
+  sprintf(buffer, "%" PRIu32, this->minute);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  second: ");
+  sprintf(buffer, "%" PRIu32, this->second);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void TimeCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("TimeCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  hour: ");
+  sprintf(buffer, "%" PRIu32, this->hour);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  minute: ");
+  sprintf(buffer, "%" PRIu32, this->minute);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  second: ");
+  sprintf(buffer, "%" PRIu32, this->second);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_EVENT
+void ListEntitiesEventResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesEventResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  device_class: ");
+  out.append("'").append(this->device_class).append("'");
+  out.append("\n");
+
+  for (const auto &it : this->event_types) {
+    out.append("  event_types: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void EventResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("EventResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  event_type: ");
+  out.append("'").append(this->event_type).append("'");
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_VALVE
+void ListEntitiesValveResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesValveResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  device_class: ");
+  out.append("'").append(this->device_class).append("'");
+  out.append("\n");
+
+  out.append("  assumed_state: ");
+  out.append(YESNO(this->assumed_state));
+  out.append("\n");
+
+  out.append("  supports_position: ");
+  out.append(YESNO(this->supports_position));
+  out.append("\n");
+
+  out.append("  supports_stop: ");
+  out.append(YESNO(this->supports_stop));
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void ValveStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ValveStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  position: ");
+  sprintf(buffer, "%g", this->position);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  current_operation: ");
+  out.append(proto_enum_to_string(this->current_operation));
+  out.append("\n");
+  out.append("}");
+}
+void ValveCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ValveCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_position: ");
+  out.append(YESNO(this->has_position));
+  out.append("\n");
+
+  out.append("  position: ");
+  sprintf(buffer, "%g", this->position);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  stop: ");
+  out.append(YESNO(this->stop));
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_DATETIME_DATETIME
+void ListEntitiesDateTimeResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesDateTimeResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void DateTimeStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("DateTimeStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  missing_state: ");
+  out.append(YESNO(this->missing_state));
+  out.append("\n");
+
+  out.append("  epoch_seconds: ");
+  sprintf(buffer, "%" PRIu32, this->epoch_seconds);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void DateTimeCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("DateTimeCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  epoch_seconds: ");
+  sprintf(buffer, "%" PRIu32, this->epoch_seconds);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+#endif
+#ifdef USE_UPDATE
+void ListEntitiesUpdateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesUpdateResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string(this->entity_category));
+  out.append("\n");
+
+  out.append("  device_class: ");
+  out.append("'").append(this->device_class).append("'");
+  out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+void UpdateStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("UpdateStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  missing_state: ");
+  out.append(YESNO(this->missing_state));
+  out.append("\n");
+
+  out.append("  in_progress: ");
+  out.append(YESNO(this->in_progress));
+  out.append("\n");
+
+  out.append("  has_progress: ");
+  out.append(YESNO(this->has_progress));
+  out.append("\n");
+
+  out.append("  progress: ");
+  sprintf(buffer, "%g", this->progress);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  current_version: ");
+  out.append("'").append(this->current_version).append("'");
+  out.append("\n");
+
+  out.append("  latest_version: ");
+  out.append("'").append(this->latest_version).append("'");
+  out.append("\n");
+
+  out.append("  title: ");
+  out.append("'").append(this->title).append("'");
+  out.append("\n");
+
+  out.append("  release_summary: ");
+  out.append("'").append(this->release_summary).append("'");
+  out.append("\n");
+
+  out.append("  release_url: ");
+  out.append("'").append(this->release_url).append("'");
+  out.append("\n");
+  out.append("}");
+}
+void UpdateCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("UpdateCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  command: ");
+  out.append(proto_enum_to_string(this->command));
+  out.append("\n");
+  out.append("}");
+}
+#endif
+
+}  // namespace api
+}  // namespace esphome
+
+#endif  // HAS_PROTO_MESSAGE_DUMP
diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h
index 3cc774f91c..8c870e5e1c 100644
--- a/esphome/components/api/api_pb2_service.h
+++ b/esphome/components/api/api_pb2_service.h
@@ -2,9 +2,10 @@
 // See script/api_protobuf/api_protobuf.py
 #pragma once
 
-#include "api_pb2.h"
 #include "esphome/core/defines.h"
 
+#include "api_pb2.h"
+
 namespace esphome {
 namespace api {
 
diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py
index 56a46a7701..2266dda81c 100755
--- a/script/api_protobuf/api_protobuf.py
+++ b/script/api_protobuf/api_protobuf.py
@@ -813,27 +813,137 @@ class RepeatedTypeInfo(TypeInfo):
         return underlying_size * 2
 
 
-def build_enum_type(desc) -> tuple[str, str]:
-    """Builds the enum type."""
+def build_type_usage_map(
+    file_desc: descriptor.FileDescriptorProto,
+) -> tuple[dict[str, str | None], dict[str, str | None]]:
+    """Build mappings for both enums and messages to their ifdefs based on usage.
+
+    Returns:
+        tuple: (enum_ifdef_map, message_ifdef_map)
+    """
+    enum_ifdef_map: dict[str, str | None] = {}
+    message_ifdef_map: dict[str, str | None] = {}
+
+    # Build maps of which types are used by which messages
+    enum_usage: dict[
+        str, set[str]
+    ] = {}  # enum_name -> set of message names that use it
+    message_usage: dict[
+        str, set[str]
+    ] = {}  # message_name -> set of message names that use it
+
+    # Build message name to ifdef mapping for quick lookup
+    message_to_ifdef: dict[str, str | None] = {
+        msg.name: get_opt(msg, pb.ifdef) for msg in file_desc.message_type
+    }
+
+    # Analyze field usage
+    for message in file_desc.message_type:
+        for field in message.field:
+            type_name = field.type_name.split(".")[-1] if field.type_name else None
+            if not type_name:
+                continue
+
+            # Track enum usage
+            if field.type == 14:  # TYPE_ENUM
+                enum_usage.setdefault(type_name, set()).add(message.name)
+            # Track message usage
+            elif field.type == 11:  # TYPE_MESSAGE
+                message_usage.setdefault(type_name, set()).add(message.name)
+
+    # Helper to get unique ifdef from a set of messages
+    def get_unique_ifdef(message_names: set[str]) -> str | None:
+        ifdefs: set[str] = {
+            message_to_ifdef[name]
+            for name in message_names
+            if message_to_ifdef.get(name)
+        }
+        return ifdefs.pop() if len(ifdefs) == 1 else None
+
+    # Build enum ifdef map
+    for enum in file_desc.enum_type:
+        if enum.name in enum_usage:
+            enum_ifdef_map[enum.name] = get_unique_ifdef(enum_usage[enum.name])
+        else:
+            enum_ifdef_map[enum.name] = None
+
+    # Build message ifdef map
+    for message in file_desc.message_type:
+        # Explicit ifdef takes precedence
+        explicit_ifdef = message_to_ifdef.get(message.name)
+        if explicit_ifdef:
+            message_ifdef_map[message.name] = explicit_ifdef
+        elif message.name in message_usage:
+            # Inherit ifdef if all parent messages have the same one
+            message_ifdef_map[message.name] = get_unique_ifdef(
+                message_usage[message.name]
+            )
+        else:
+            message_ifdef_map[message.name] = None
+
+    # Second pass: propagate ifdefs recursively
+    # Keep iterating until no more changes are made
+    changed = True
+    iterations = 0
+    while changed and iterations < 10:  # Add safety limit
+        changed = False
+        iterations += 1
+        for message in file_desc.message_type:
+            # Skip if already has an ifdef
+            if message_ifdef_map.get(message.name):
+                continue
+
+            # Check if this message is used by other messages
+            if message.name not in message_usage:
+                continue
+
+            # Get ifdefs from all messages that use this one
+            parent_ifdefs: set[str] = {
+                message_ifdef_map.get(parent)
+                for parent in message_usage[message.name]
+                if message_ifdef_map.get(parent)
+            }
+
+            # If all parents have the same ifdef, inherit it
+            if len(parent_ifdefs) == 1 and None not in parent_ifdefs:
+                message_ifdef_map[message.name] = parent_ifdefs.pop()
+                changed = True
+
+    return enum_ifdef_map, message_ifdef_map
+
+
+def build_enum_type(desc, enum_ifdef_map) -> tuple[str, str, str]:
+    """Builds the enum type.
+
+    Args:
+        desc: The enum descriptor
+        enum_ifdef_map: Mapping of enum names to their ifdefs
+
+    Returns:
+        tuple: (header_content, cpp_content, dump_cpp_content)
+    """
     name = desc.name
+
     out = f"enum {name} : uint32_t {{\n"
     for v in desc.value:
         out += f"  {v.name} = {v.number},\n"
     out += "};\n"
 
-    cpp = "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
-    cpp += f"template<> const char *proto_enum_to_string(enums::{name} value) {{\n"
-    cpp += "  switch (value) {\n"
-    for v in desc.value:
-        cpp += f"    case enums::{v.name}:\n"
-        cpp += f'      return "{v.name}";\n'
-    cpp += "    default:\n"
-    cpp += '      return "UNKNOWN";\n'
-    cpp += "  }\n"
-    cpp += "}\n"
-    cpp += "#endif\n"
+    # Regular cpp file has no enum content anymore
+    cpp = ""
 
-    return out, cpp
+    # Dump cpp content for enum string conversion
+    dump_cpp = f"template<> const char *proto_enum_to_string(enums::{name} value) {{\n"
+    dump_cpp += "  switch (value) {\n"
+    for v in desc.value:
+        dump_cpp += f"    case enums::{v.name}:\n"
+        dump_cpp += f'      return "{v.name}";\n'
+    dump_cpp += "    default:\n"
+    dump_cpp += '      return "UNKNOWN";\n'
+    dump_cpp += "  }\n"
+    dump_cpp += "}\n"
+
+    return out, cpp, dump_cpp
 
 
 def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int:
@@ -855,7 +965,7 @@ def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int:
 def build_message_type(
     desc: descriptor.DescriptorProto,
     base_class_fields: dict[str, list[descriptor.FieldDescriptorProto]] = None,
-) -> tuple[str, str]:
+) -> tuple[str, str, str]:
     public_content: list[str] = []
     protected_content: list[str] = []
     decode_varint: list[str] = []
@@ -886,7 +996,7 @@ def build_message_type(
             f"static constexpr uint16_t ESTIMATED_SIZE = {estimated_size};"
         )
 
-        # Add message_name method for debugging
+        # Add message_name method inline in header
         public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP")
         snake_name = camel_to_snake(desc.name)
         public_content.append(
@@ -993,32 +1103,32 @@ def build_message_type(
         public_content.append(prot)
     # If no fields to calculate size for, the default implementation in ProtoMessage will be used
 
-    o = f"void {desc.name}::dump_to(std::string &out) const {{"
-    if dump:
-        if len(dump) == 1 and len(dump[0]) + len(o) + 3 < 120:
-            o += f" {dump[0]} "
-        else:
-            o += "\n"
-            o += "  __attribute__((unused)) char buffer[64];\n"
-            o += f'  out.append("{desc.name} {{\\n");\n'
-            o += indent("\n".join(dump)) + "\n"
-            o += '  out.append("}");\n'
-    else:
-        o2 = f'out.append("{desc.name} {{}}");'
-        if len(o) + len(o2) + 3 < 120:
-            o += f" {o2} "
-        else:
-            o += "\n"
-            o += f"  {o2}\n"
-    o += "}\n"
-    cpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
-    cpp += o
-    cpp += "#endif\n"
+    # dump_to method declaration in header
     prot = "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
     prot += "void dump_to(std::string &out) const override;\n"
     prot += "#endif\n"
     public_content.append(prot)
 
+    # dump_to implementation will go in dump_cpp
+    dump_impl = f"void {desc.name}::dump_to(std::string &out) const {{"
+    if dump:
+        if len(dump) == 1 and len(dump[0]) + len(dump_impl) + 3 < 120:
+            dump_impl += f" {dump[0]} "
+        else:
+            dump_impl += "\n"
+            dump_impl += "  __attribute__((unused)) char buffer[64];\n"
+            dump_impl += f'  out.append("{desc.name} {{\\n");\n'
+            dump_impl += indent("\n".join(dump)) + "\n"
+            dump_impl += '  out.append("}");\n'
+    else:
+        o2 = f'out.append("{desc.name} {{}}");'
+        if len(dump_impl) + len(o2) + 3 < 120:
+            dump_impl += f" {o2} "
+        else:
+            dump_impl += "\n"
+            dump_impl += f"  {o2}\n"
+    dump_impl += "}\n"
+
     if base_class:
         out = f"class {desc.name} : public {base_class} {{\n"
     else:
@@ -1031,7 +1141,11 @@ def build_message_type(
     if len(protected_content) > 0:
         out += "\n"
     out += "};\n"
-    return out, cpp
+
+    # Build dump_cpp content with dump_to implementation
+    dump_cpp = dump_impl
+
+    return out, cpp, dump_cpp
 
 
 SOURCE_BOTH = 0
@@ -1119,7 +1233,7 @@ def find_common_fields(
 def build_base_class(
     base_class_name: str,
     common_fields: list[descriptor.FieldDescriptorProto],
-) -> tuple[str, str]:
+) -> tuple[str, str, str]:
     """Build the base class definition and implementation."""
     public_content = []
     protected_content = []
@@ -1156,16 +1270,18 @@ def build_base_class(
     out += "};\n"
 
     # No implementation needed for base classes
+    dump_cpp = ""
 
-    return out, cpp
+    return out, cpp, dump_cpp
 
 
 def generate_base_classes(
     base_class_groups: dict[str, list[descriptor.DescriptorProto]],
-) -> tuple[str, str]:
+) -> tuple[str, str, str]:
     """Generate all base classes."""
     all_headers = []
     all_cpp = []
+    all_dump_cpp = []
 
     for base_class_name, messages in base_class_groups.items():
         # Find common fields
@@ -1173,11 +1289,12 @@ def generate_base_classes(
 
         if common_fields:
             # Generate base class
-            header, cpp = build_base_class(base_class_name, common_fields)
+            header, cpp, dump_cpp = build_base_class(base_class_name, common_fields)
             all_headers.append(header)
             all_cpp.append(cpp)
+            all_dump_cpp.append(dump_cpp)
 
-    return "\n".join(all_headers), "\n".join(all_cpp)
+    return "\n".join(all_headers), "\n".join(all_cpp), "\n".join(all_dump_cpp)
 
 
 def build_service_message_type(
@@ -1244,15 +1361,17 @@ def main() -> None:
     file = d.file[0]
     content = FILE_HEADER
     content += """\
-    #pragma once
+#pragma once
 
-    #include "proto.h"
-    #include "api_pb2_size.h"
+#include "esphome/core/defines.h"
 
-    namespace esphome {
-    namespace api {
+#include "proto.h"
+#include "api_pb2_size.h"
 
-    """
+namespace esphome {
+namespace api {
+
+"""
 
     cpp = FILE_HEADER
     cpp += """\
@@ -1261,19 +1380,56 @@ def main() -> None:
     #include "esphome/core/log.h"
     #include "esphome/core/helpers.h"
 
-    #include 
+namespace esphome {
+namespace api {
 
-    namespace esphome {
-    namespace api {
+"""
 
-    """
+    # Initialize dump cpp content
+    dump_cpp = FILE_HEADER
+    dump_cpp += """\
+#include "api_pb2.h"
+#include "esphome/core/helpers.h"
+
+#include 
+
+#ifdef HAS_PROTO_MESSAGE_DUMP
+
+namespace esphome {
+namespace api {
+
+"""
 
     content += "namespace enums {\n\n"
 
+    # Build dynamic ifdef mappings for both enums and messages
+    enum_ifdef_map, message_ifdef_map = build_type_usage_map(file)
+
+    # Simple grouping of enums by ifdef
+    current_ifdef = None
+
     for enum in file.enum_type:
-        s, c = build_enum_type(enum)
+        s, c, dc = build_enum_type(enum, enum_ifdef_map)
+        enum_ifdef = enum_ifdef_map.get(enum.name)
+
+        # Handle ifdef changes
+        if enum_ifdef != current_ifdef:
+            if current_ifdef is not None:
+                content += "#endif\n"
+                dump_cpp += "#endif\n"
+            if enum_ifdef is not None:
+                content += f"#ifdef {enum_ifdef}\n"
+                dump_cpp += f"#ifdef {enum_ifdef}\n"
+            current_ifdef = enum_ifdef
+
         content += s
         cpp += c
+        dump_cpp += dc
+
+    # Close last ifdef
+    if current_ifdef is not None:
+        content += "#endif\n"
+        dump_cpp += "#endif\n"
 
     content += "\n}  // namespace enums\n\n"
 
@@ -1291,26 +1447,61 @@ def main() -> None:
 
     # Generate base classes
     if base_class_fields:
-        base_headers, base_cpp = generate_base_classes(base_class_groups)
+        base_headers, base_cpp, base_dump_cpp = generate_base_classes(base_class_groups)
         content += base_headers
         cpp += base_cpp
+        dump_cpp += base_dump_cpp
 
     # Generate message types with base class information
+    # Simple grouping by ifdef
+    current_ifdef = None
+
     for m in mt:
-        s, c = build_message_type(m, base_class_fields)
+        s, c, dc = build_message_type(m, base_class_fields)
+        msg_ifdef = message_ifdef_map.get(m.name)
+
+        # Handle ifdef changes
+        if msg_ifdef != current_ifdef:
+            if current_ifdef is not None:
+                content += "#endif\n"
+                if cpp:
+                    cpp += "#endif\n"
+                if dump_cpp:
+                    dump_cpp += "#endif\n"
+            if msg_ifdef is not None:
+                content += f"#ifdef {msg_ifdef}\n"
+                cpp += f"#ifdef {msg_ifdef}\n"
+                dump_cpp += f"#ifdef {msg_ifdef}\n"
+            current_ifdef = msg_ifdef
+
         content += s
         cpp += c
+        dump_cpp += dc
+
+    # Close last ifdef
+    if current_ifdef is not None:
+        content += "#endif\n"
+        cpp += "#endif\n"
+        dump_cpp += "#endif\n"
 
     content += """\
 
-    }  // namespace api
-    }  // namespace esphome
-    """
+}  // namespace api
+}  // namespace esphome
+"""
     cpp += """\
 
-    }  // namespace api
-    }  // namespace esphome
-    """
+}  // namespace api
+}  // namespace esphome
+"""
+
+    dump_cpp += """\
+
+}  // namespace api
+}  // namespace esphome
+
+#endif  // HAS_PROTO_MESSAGE_DUMP
+"""
 
     with open(root / "api_pb2.h", "w", encoding="utf-8") as f:
         f.write(content)
@@ -1318,29 +1509,33 @@ def main() -> None:
     with open(root / "api_pb2.cpp", "w", encoding="utf-8") as f:
         f.write(cpp)
 
+    with open(root / "api_pb2_dump.cpp", "w", encoding="utf-8") as f:
+        f.write(dump_cpp)
+
     hpp = FILE_HEADER
     hpp += """\
-    #pragma once
+#pragma once
 
-    #include "api_pb2.h"
-    #include "esphome/core/defines.h"
+#include "esphome/core/defines.h"
 
-    namespace esphome {
-    namespace api {
+#include "api_pb2.h"
 
-    """
+namespace esphome {
+namespace api {
+
+"""
 
     cpp = FILE_HEADER
     cpp += """\
-    #include "api_pb2_service.h"
-    #include "esphome/core/log.h"
+#include "api_pb2_service.h"
+#include "esphome/core/log.h"
 
-    namespace esphome {
-    namespace api {
+namespace esphome {
+namespace api {
 
-    static const char *const TAG = "api.service";
+static const char *const TAG = "api.service";
 
-    """
+"""
 
     class_name = "APIServerConnectionBase"
 
@@ -1419,7 +1614,7 @@ def main() -> None:
         needs_conn = get_opt(m, pb.needs_setup_connection, True)
         needs_auth = get_opt(m, pb.needs_authentication, True)
 
-        ifdef = ifdefs.get(inp, None)
+        ifdef = message_ifdef_map.get(inp, ifdefs.get(inp, None))
 
         if ifdef is not None:
             hpp += f"#ifdef {ifdef}\n"
@@ -1476,14 +1671,14 @@ def main() -> None:
 
     hpp += """\
 
-    }  // namespace api
-    }  // namespace esphome
-    """
+}  // namespace api
+}  // namespace esphome
+"""
     cpp += """\
 
-    }  // namespace api
-    }  // namespace esphome
-    """
+}  // namespace api
+}  // namespace esphome
+"""
 
     with open(root / "api_pb2_service.h", "w", encoding="utf-8") as f:
         f.write(hpp)
@@ -1506,6 +1701,8 @@ def main() -> None:
         exec_clang_format(root / "api_pb2_service.cpp")
         exec_clang_format(root / "api_pb2.h")
         exec_clang_format(root / "api_pb2.cpp")
+        exec_clang_format(root / "api_pb2_dump.h")
+        exec_clang_format(root / "api_pb2_dump.cpp")
     except ImportError:
         pass
 

From 5b55e205efb9ddf25dc69a28ca3f56a73ed9fe31 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Wed, 2 Jul 2025 16:42:08 -0500
Subject: [PATCH 218/293] Save flash and RAM by conditionally compiling unused
 API password code (#9297)

---
 esphome/components/api/__init__.py        | 4 +++-
 esphome/components/api/api_connection.cpp | 9 ++++++++-
 esphome/components/api/api_server.cpp     | 4 ++++
 esphome/components/api/api_server.h       | 6 +++++-
 4 files changed, 20 insertions(+), 3 deletions(-)

diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py
index b02a875d72..2f1be28293 100644
--- a/esphome/components/api/__init__.py
+++ b/esphome/components/api/__init__.py
@@ -132,7 +132,9 @@ async def to_code(config):
     await cg.register_component(var, config)
 
     cg.add(var.set_port(config[CONF_PORT]))
-    cg.add(var.set_password(config[CONF_PASSWORD]))
+    if config[CONF_PASSWORD]:
+        cg.add_define("USE_API_PASSWORD")
+        cg.add(var.set_password(config[CONF_PASSWORD]))
     cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
     cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
 
diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp
index e83d508c50..49ad9706bc 100644
--- a/esphome/components/api/api_connection.cpp
+++ b/esphome/components/api/api_connection.cpp
@@ -1503,7 +1503,10 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) {
   return resp;
 }
 ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
-  bool correct = this->parent_->check_password(msg.password);
+  bool correct = true;
+#ifdef USE_API_PASSWORD
+  correct = this->parent_->check_password(msg.password);
+#endif
 
   ConnectResponse resp;
   // bool invalid_password = 1;
@@ -1524,7 +1527,11 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
 }
 DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
   DeviceInfoResponse resp{};
+#ifdef USE_API_PASSWORD
   resp.uses_password = this->parent_->uses_password();
+#else
+  resp.uses_password = false;
+#endif
   resp.name = App.get_name();
   resp.friendly_name = App.get_friendly_name();
   resp.suggested_area = App.get_area();
diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp
index ebe80604dc..0fd9c1a228 100644
--- a/esphome/components/api/api_server.cpp
+++ b/esphome/components/api/api_server.cpp
@@ -218,6 +218,7 @@ void APIServer::dump_config() {
 #endif
 }
 
+#ifdef USE_API_PASSWORD
 bool APIServer::uses_password() const { return !this->password_.empty(); }
 
 bool APIServer::check_password(const std::string &password) const {
@@ -248,6 +249,7 @@ bool APIServer::check_password(const std::string &password) const {
 
   return result == 0;
 }
+#endif
 
 void APIServer::handle_disconnect(APIConnection *conn) {}
 
@@ -431,7 +433,9 @@ float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI;
 
 void APIServer::set_port(uint16_t port) { this->port_ = port; }
 
+#ifdef USE_API_PASSWORD
 void APIServer::set_password(const std::string &password) { this->password_ = password; }
+#endif
 
 void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
 
diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h
index 5a9b0677bc..9dc2b4b7d6 100644
--- a/esphome/components/api/api_server.h
+++ b/esphome/components/api/api_server.h
@@ -35,10 +35,12 @@ class APIServer : public Component, public Controller {
   void dump_config() override;
   void on_shutdown() override;
   bool teardown() override;
+#ifdef USE_API_PASSWORD
   bool check_password(const std::string &password) const;
   bool uses_password() const;
-  void set_port(uint16_t port);
   void set_password(const std::string &password);
+#endif
+  void set_port(uint16_t port);
   void set_reboot_timeout(uint32_t reboot_timeout);
   void set_batch_delay(uint16_t batch_delay);
   uint16_t get_batch_delay() const { return batch_delay_; }
@@ -179,7 +181,9 @@ class APIServer : public Component, public Controller {
 
   // Vectors and strings (12 bytes each on 32-bit)
   std::vector> clients_;
+#ifdef USE_API_PASSWORD
   std::string password_;
+#endif
   std::vector shared_write_buffer_;  // Shared proto write buffer for all connections
   std::vector state_subs_;
 #ifdef USE_API_YAML_SERVICES

From 798eef41b9084f266f4f9d89582c051de6efedfa Mon Sep 17 00:00:00 2001
From: DanielV 
Date: Thu, 3 Jul 2025 03:25:46 +0200
Subject: [PATCH 219/293] [Packet transport] Ping timeout sensor (#8694)

---
 .../packet_transport/binary_sensor.py         | 65 +++++++++++++++++--
 .../packet_transport/packet_transport.cpp     | 32 ++++++++-
 .../packet_transport/packet_transport.h       | 16 +++--
 esphome/core/defines.h                        |  1 +
 tests/components/packet_transport/common.yaml |  4 ++
 5 files changed, 108 insertions(+), 10 deletions(-)

diff --git a/esphome/components/packet_transport/binary_sensor.py b/esphome/components/packet_transport/binary_sensor.py
index 076e37e6bb..09bbf91c99 100644
--- a/esphome/components/packet_transport/binary_sensor.py
+++ b/esphome/components/packet_transport/binary_sensor.py
@@ -1,19 +1,76 @@
 import esphome.codegen as cg
 from esphome.components import binary_sensor
-from esphome.const import CONF_ID
+import esphome.config_validation as cv
+from esphome.const import (
+    CONF_DATA,
+    CONF_ID,
+    CONF_NAME,
+    CONF_STATUS,
+    CONF_TYPE,
+    DEVICE_CLASS_CONNECTIVITY,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+)
+import esphome.final_validate as fv
 
 from . import (
+    CONF_ENCRYPTION,
+    CONF_PING_PONG_ENABLE,
     CONF_PROVIDER,
+    CONF_PROVIDERS,
     CONF_REMOTE_ID,
     CONF_TRANSPORT_ID,
+    PacketTransport,
     packet_transport_sensor_schema,
+    provider_name_validate,
 )
 
-CONFIG_SCHEMA = packet_transport_sensor_schema(binary_sensor.binary_sensor_schema())
+STATUS_SENSOR_SCHEMA = binary_sensor.binary_sensor_schema(
+    device_class=DEVICE_CLASS_CONNECTIVITY,
+    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+).extend(
+    {
+        cv.GenerateID(CONF_TRANSPORT_ID): cv.use_id(PacketTransport),
+        cv.Required(CONF_PROVIDER): provider_name_validate,
+    }
+)
+
+CONFIG_SCHEMA = cv.typed_schema(
+    {
+        CONF_DATA: packet_transport_sensor_schema(binary_sensor.binary_sensor_schema()),
+        CONF_STATUS: STATUS_SENSOR_SCHEMA,
+    },
+    key=CONF_TYPE,
+    default_type=CONF_DATA,
+)
+
+
+def _final_validate(config):
+    if config[CONF_TYPE] != CONF_STATUS:
+        # Only run this validation if a status sensor is being configured
+        return config
+    full_config = fv.full_config.get()
+    transport_path = full_config.get_path_for_id(config[CONF_TRANSPORT_ID])[:-1]
+    transport_config = full_config.get_config_for_path(transport_path)
+    if transport_config[CONF_PING_PONG_ENABLE] and any(
+        CONF_ENCRYPTION in p
+        for p in transport_config[CONF_PROVIDERS]
+        if p[CONF_NAME] == config[CONF_PROVIDER]
+    ):
+        return config
+    raise cv.Invalid(
+        "Status sensor requires ping-pong to be enabled and the nominated provider to use encryption."
+    )
+
+
+FINAL_VALIDATE_SCHEMA = _final_validate
 
 
 async def to_code(config):
     var = await binary_sensor.new_binary_sensor(config)
     comp = await cg.get_variable(config[CONF_TRANSPORT_ID])
-    remote_id = str(config.get(CONF_REMOTE_ID) or config.get(CONF_ID))
-    cg.add(comp.add_remote_binary_sensor(config[CONF_PROVIDER], remote_id, var))
+    if config[CONF_TYPE] == CONF_STATUS:
+        cg.add(comp.set_provider_status_sensor(config[CONF_PROVIDER], var))
+        cg.add_define("USE_STATUS_SENSOR")
+    else:  # CONF_DATA is default
+        remote_id = str(config.get(CONF_REMOTE_ID) or config.get(CONF_ID))
+        cg.add(comp.add_remote_binary_sensor(config[CONF_PROVIDER], remote_id, var))
diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp
index 5c721002b0..6684d43ff7 100644
--- a/esphome/components/packet_transport/packet_transport.cpp
+++ b/esphome/components/packet_transport/packet_transport.cpp
@@ -317,8 +317,37 @@ void PacketTransport::update() {
   auto now = millis() / 1000;
   if (this->last_key_time_ + this->ping_pong_recyle_time_ < now) {
     this->resend_ping_key_ = this->ping_pong_enable_;
+    ESP_LOGV(TAG, "Ping request, age %u", now - this->last_key_time_);
     this->last_key_time_ = now;
   }
+  for (const auto &provider : this->providers_) {
+    uint32_t key_response_age = now - provider.second.last_key_response_time;
+    if (key_response_age > (this->ping_pong_recyle_time_ * 2u)) {
+#ifdef USE_STATUS_SENSOR
+      if (provider.second.status_sensor != nullptr && provider.second.status_sensor->state) {
+        ESP_LOGI(TAG, "Ping status for %s timeout at %u with age %u", provider.first.c_str(), now, key_response_age);
+        provider.second.status_sensor->publish_state(false);
+      }
+#endif
+#ifdef USE_SENSOR
+      for (auto &sensor : this->remote_sensors_[provider.first]) {
+        sensor.second->publish_state(NAN);
+      }
+#endif
+#ifdef USE_BINARY_SENSOR
+      for (auto &sensor : this->remote_binary_sensors_[provider.first]) {
+        sensor.second->invalidate_state();
+      }
+#endif
+    } else {
+#ifdef USE_STATUS_SENSOR
+      if (provider.second.status_sensor != nullptr && !provider.second.status_sensor->state) {
+        ESP_LOGI(TAG, "Ping status for %s restored at %u with age %u", provider.first.c_str(), now, key_response_age);
+        provider.second.status_sensor->publish_state(true);
+      }
+#endif
+    }
+  }
 }
 
 void PacketTransport::add_key_(const char *name, uint32_t key) {
@@ -437,7 +466,8 @@ void PacketTransport::process_(const std::vector &data) {
     if (decoder.decode(PING_KEY, key) == DECODE_OK) {
       if (key == this->ping_key_) {
         ping_key_seen = true;
-        ESP_LOGV(TAG, "Found good ping key %X", (unsigned) key);
+        provider.last_key_response_time = millis() / 1000;
+        ESP_LOGV(TAG, "Found good ping key %X at timestamp %" PRIu32, (unsigned) key, provider.last_key_response_time);
       } else {
         ESP_LOGV(TAG, "Unknown ping key %X", (unsigned) key);
       }
diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h
index 34edb82963..a2370e9749 100644
--- a/esphome/components/packet_transport/packet_transport.h
+++ b/esphome/components/packet_transport/packet_transport.h
@@ -8,7 +8,7 @@
 #ifdef USE_BINARY_SENSOR
 #include "esphome/components/binary_sensor/binary_sensor.h"
 #endif
-#
+
 #include 
 #include 
 
@@ -27,6 +27,10 @@ struct Provider {
   std::vector encryption_key;
   const char *name;
   uint32_t last_code[2];
+  uint32_t last_key_response_time;
+#ifdef USE_STATUS_SENSOR
+  binary_sensor::BinarySensor *status_sensor{nullptr};
+#endif
 };
 
 #ifdef USE_SENSOR
@@ -75,10 +79,7 @@ class PacketTransport : public PollingComponent {
 
   void add_provider(const char *hostname) {
     if (this->providers_.count(hostname) == 0) {
-      Provider provider;
-      provider.encryption_key = std::vector{};
-      provider.last_code[0] = 0;
-      provider.last_code[1] = 0;
+      Provider provider{};
       provider.name = hostname;
       this->providers_[hostname] = provider;
 #ifdef USE_SENSOR
@@ -97,6 +98,11 @@ class PacketTransport : public PollingComponent {
   void set_provider_encryption(const char *name, std::vector key) {
     this->providers_[name].encryption_key = std::move(key);
   }
+#ifdef USE_STATUS_SENSOR
+  void set_provider_status_sensor(const char *name, binary_sensor::BinarySensor *sensor) {
+    this->providers_[name].status_sensor = sensor;
+  }
+#endif
   void set_platform_name(const char *name) { this->platform_name_ = name; }
 
  protected:
diff --git a/esphome/core/defines.h b/esphome/core/defines.h
index be872689f3..320b40dc90 100644
--- a/esphome/core/defines.h
+++ b/esphome/core/defines.h
@@ -86,6 +86,7 @@
 #define USE_SELECT
 #define USE_SENSOR
 #define USE_STATUS_LED
+#define USE_STATUS_SENSOR
 #define USE_SWITCH
 #define USE_TEXT
 #define USE_TEXT_SENSOR
diff --git a/tests/components/packet_transport/common.yaml b/tests/components/packet_transport/common.yaml
index cbb34c4572..9151cf27dc 100644
--- a/tests/components/packet_transport/common.yaml
+++ b/tests/components/packet_transport/common.yaml
@@ -36,5 +36,9 @@ binary_sensor:
   - platform: packet_transport
     provider: unencrypted-device
     id: other_binary_sensor_id
+  - platform: packet_transport
+    provider: some-device-name
+    type: status
+    name: Some-Device Status
   - platform: template
     id: binary_sensor_id1

From 34db02661c5df454f143720155058cebef4db80b Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Wed, 2 Jul 2025 21:50:53 -0500
Subject: [PATCH 220/293] Allow disabling API batch delay for real-time state
 updates (#9298)

---
 esphome/components/api/api_connection.cpp     | 88 ++++++++++---------
 esphome/components/api/api_connection.h       | 60 ++++++++++++-
 .../batch_delay_zero_rapid_transitions.yaml   | 43 +++++++++
 ...test_batch_delay_zero_rapid_transitions.py | 58 ++++++++++++
 .../test_host_mode_empty_string_options.py    | 42 +++++----
 .../integration/test_host_mode_fan_preset.py  |  8 ++
 .../test_host_mode_many_entities.py           | 37 +++++---
 7 files changed, 261 insertions(+), 75 deletions(-)
 create mode 100644 tests/integration/fixtures/batch_delay_zero_rapid_transitions.yaml
 create mode 100644 tests/integration/test_batch_delay_zero_rapid_transitions.py

diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp
index 49ad9706bc..4d99bdbbd6 100644
--- a/esphome/components/api/api_connection.cpp
+++ b/esphome/components/api/api_connection.cpp
@@ -90,19 +90,6 @@ APIConnection::~APIConnection() {
 #endif
 }
 
-#ifdef HAS_PROTO_MESSAGE_DUMP
-void APIConnection::log_batch_item_(const DeferredBatch::BatchItem &item) {
-  // Set log-only mode
-  this->flags_.log_only_mode = true;
-
-  // Call the creator - it will create the message and log it via encode_message_to_buffer
-  item.creator(item.entity, this, std::numeric_limits::max(), true, item.message_type);
-
-  // Clear log-only mode
-  this->flags_.log_only_mode = false;
-}
-#endif
-
 void APIConnection::loop() {
   if (this->flags_.next_close) {
     // requested a disconnect
@@ -154,15 +141,25 @@ void APIConnection::loop() {
     }
   }
 
-  // Process deferred batch if scheduled
+  // Process deferred batch if scheduled and timer has expired
   if (this->flags_.batch_scheduled && now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) {
     this->process_batch_();
   }
 
   if (!this->list_entities_iterator_.completed()) {
-    this->list_entities_iterator_.advance();
+    this->process_iterator_batch_(this->list_entities_iterator_);
   } else if (!this->initial_state_iterator_.completed()) {
-    this->initial_state_iterator_.advance();
+    this->process_iterator_batch_(this->initial_state_iterator_);
+
+    // If we've completed initial states, process any remaining and clear the flag
+    if (this->initial_state_iterator_.completed()) {
+      // Process any remaining batched messages immediately
+      if (!this->deferred_batch_.empty()) {
+        this->process_batch_();
+      }
+      // Now that everything is sent, enable immediate sending for future state changes
+      this->flags_.should_try_send_immediately = true;
+    }
   }
 
   if (this->flags_.sent_ping) {
@@ -300,8 +297,8 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes
 
 #ifdef USE_BINARY_SENSOR
 bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor) {
-  return this->schedule_message_(binary_sensor, &APIConnection::try_send_binary_sensor_state,
-                                 BinarySensorStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(binary_sensor, &APIConnection::try_send_binary_sensor_state,
+                                   BinarySensorStateResponse::MESSAGE_TYPE);
 }
 
 uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -328,7 +325,7 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne
 
 #ifdef USE_COVER
 bool APIConnection::send_cover_state(cover::Cover *cover) {
-  return this->schedule_message_(cover, &APIConnection::try_send_cover_state, CoverStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(cover, &APIConnection::try_send_cover_state, CoverStateResponse::MESSAGE_TYPE);
 }
 uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
                                              bool is_single) {
@@ -389,7 +386,7 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) {
 
 #ifdef USE_FAN
 bool APIConnection::send_fan_state(fan::Fan *fan) {
-  return this->schedule_message_(fan, &APIConnection::try_send_fan_state, FanStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(fan, &APIConnection::try_send_fan_state, FanStateResponse::MESSAGE_TYPE);
 }
 uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
                                            bool is_single) {
@@ -448,7 +445,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
 
 #ifdef USE_LIGHT
 bool APIConnection::send_light_state(light::LightState *light) {
-  return this->schedule_message_(light, &APIConnection::try_send_light_state, LightStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(light, &APIConnection::try_send_light_state, LightStateResponse::MESSAGE_TYPE);
 }
 uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
                                              bool is_single) {
@@ -540,7 +537,7 @@ void APIConnection::light_command(const LightCommandRequest &msg) {
 
 #ifdef USE_SENSOR
 bool APIConnection::send_sensor_state(sensor::Sensor *sensor) {
-  return this->schedule_message_(sensor, &APIConnection::try_send_sensor_state, SensorStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(sensor, &APIConnection::try_send_sensor_state, SensorStateResponse::MESSAGE_TYPE);
 }
 
 uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -572,7 +569,7 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *
 
 #ifdef USE_SWITCH
 bool APIConnection::send_switch_state(switch_::Switch *a_switch) {
-  return this->schedule_message_(a_switch, &APIConnection::try_send_switch_state, SwitchStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(a_switch, &APIConnection::try_send_switch_state, SwitchStateResponse::MESSAGE_TYPE);
 }
 
 uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -609,8 +606,8 @@ void APIConnection::switch_command(const SwitchCommandRequest &msg) {
 
 #ifdef USE_TEXT_SENSOR
 bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) {
-  return this->schedule_message_(text_sensor, &APIConnection::try_send_text_sensor_state,
-                                 TextSensorStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(text_sensor, &APIConnection::try_send_text_sensor_state,
+                                   TextSensorStateResponse::MESSAGE_TYPE);
 }
 
 uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -637,7 +634,7 @@ uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnect
 
 #ifdef USE_CLIMATE
 bool APIConnection::send_climate_state(climate::Climate *climate) {
-  return this->schedule_message_(climate, &APIConnection::try_send_climate_state, ClimateStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(climate, &APIConnection::try_send_climate_state, ClimateStateResponse::MESSAGE_TYPE);
 }
 uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
                                                bool is_single) {
@@ -737,7 +734,7 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
 
 #ifdef USE_NUMBER
 bool APIConnection::send_number_state(number::Number *number) {
-  return this->schedule_message_(number, &APIConnection::try_send_number_state, NumberStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(number, &APIConnection::try_send_number_state, NumberStateResponse::MESSAGE_TYPE);
 }
 
 uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -777,7 +774,7 @@ void APIConnection::number_command(const NumberCommandRequest &msg) {
 
 #ifdef USE_DATETIME_DATE
 bool APIConnection::send_date_state(datetime::DateEntity *date) {
-  return this->schedule_message_(date, &APIConnection::try_send_date_state, DateStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(date, &APIConnection::try_send_date_state, DateStateResponse::MESSAGE_TYPE);
 }
 uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
                                             bool is_single) {
@@ -811,7 +808,7 @@ void APIConnection::date_command(const DateCommandRequest &msg) {
 
 #ifdef USE_DATETIME_TIME
 bool APIConnection::send_time_state(datetime::TimeEntity *time) {
-  return this->schedule_message_(time, &APIConnection::try_send_time_state, TimeStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(time, &APIConnection::try_send_time_state, TimeStateResponse::MESSAGE_TYPE);
 }
 uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
                                             bool is_single) {
@@ -845,8 +842,8 @@ void APIConnection::time_command(const TimeCommandRequest &msg) {
 
 #ifdef USE_DATETIME_DATETIME
 bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) {
-  return this->schedule_message_(datetime, &APIConnection::try_send_datetime_state,
-                                 DateTimeStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(datetime, &APIConnection::try_send_datetime_state,
+                                   DateTimeStateResponse::MESSAGE_TYPE);
 }
 uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
                                                 bool is_single) {
@@ -881,7 +878,7 @@ void APIConnection::datetime_command(const DateTimeCommandRequest &msg) {
 
 #ifdef USE_TEXT
 bool APIConnection::send_text_state(text::Text *text) {
-  return this->schedule_message_(text, &APIConnection::try_send_text_state, TextStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(text, &APIConnection::try_send_text_state, TextStateResponse::MESSAGE_TYPE);
 }
 
 uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -919,7 +916,7 @@ void APIConnection::text_command(const TextCommandRequest &msg) {
 
 #ifdef USE_SELECT
 bool APIConnection::send_select_state(select::Select *select) {
-  return this->schedule_message_(select, &APIConnection::try_send_select_state, SelectStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(select, &APIConnection::try_send_select_state, SelectStateResponse::MESSAGE_TYPE);
 }
 
 uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -974,7 +971,7 @@ void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg
 
 #ifdef USE_LOCK
 bool APIConnection::send_lock_state(lock::Lock *a_lock) {
-  return this->schedule_message_(a_lock, &APIConnection::try_send_lock_state, LockStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(a_lock, &APIConnection::try_send_lock_state, LockStateResponse::MESSAGE_TYPE);
 }
 
 uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -1018,7 +1015,7 @@ void APIConnection::lock_command(const LockCommandRequest &msg) {
 
 #ifdef USE_VALVE
 bool APIConnection::send_valve_state(valve::Valve *valve) {
-  return this->schedule_message_(valve, &APIConnection::try_send_valve_state, ValveStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(valve, &APIConnection::try_send_valve_state, ValveStateResponse::MESSAGE_TYPE);
 }
 uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
                                              bool is_single) {
@@ -1058,8 +1055,8 @@ void APIConnection::valve_command(const ValveCommandRequest &msg) {
 
 #ifdef USE_MEDIA_PLAYER
 bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) {
-  return this->schedule_message_(media_player, &APIConnection::try_send_media_player_state,
-                                 MediaPlayerStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(media_player, &APIConnection::try_send_media_player_state,
+                                   MediaPlayerStateResponse::MESSAGE_TYPE);
 }
 uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
                                                     bool is_single) {
@@ -1320,8 +1317,8 @@ void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetCon
 
 #ifdef USE_ALARM_CONTROL_PANEL
 bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
-  return this->schedule_message_(a_alarm_control_panel, &APIConnection::try_send_alarm_control_panel_state,
-                                 AlarmControlPanelStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(a_alarm_control_panel, &APIConnection::try_send_alarm_control_panel_state,
+                                   AlarmControlPanelStateResponse::MESSAGE_TYPE);
 }
 uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn,
                                                            uint32_t remaining_size, bool is_single) {
@@ -1404,7 +1401,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
 
 #ifdef USE_UPDATE
 bool APIConnection::send_update_state(update::UpdateEntity *update) {
-  return this->schedule_message_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE);
+  return this->send_message_smart_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE);
 }
 uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
                                               bool is_single) {
@@ -1751,11 +1748,16 @@ void APIConnection::process_batch_() {
 
     if (payload_size > 0 &&
         this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) {
-      this->deferred_batch_.clear();
+#ifdef HAS_PROTO_MESSAGE_DUMP
+      // Log messages after send attempt for VV debugging
+      // It's safe to use the buffer for logging at this point regardless of send result
+      this->log_batch_item_(item);
+#endif
+      this->clear_batch_();
     } else if (payload_size == 0) {
       // Message too large
       ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type);
-      this->deferred_batch_.clear();
+      this->clear_batch_();
     }
     return;
   }
@@ -1864,7 +1866,7 @@ void APIConnection::process_batch_() {
     this->schedule_batch_();
   } else {
     // All items processed
-    this->deferred_batch_.clear();
+    this->clear_batch_();
   }
 }
 
diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h
index 151369aa70..8922aab94a 100644
--- a/esphome/components/api/api_connection.h
+++ b/esphome/components/api/api_connection.h
@@ -18,6 +18,8 @@ namespace api {
 
 // Keepalive timeout in milliseconds
 static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
+// Maximum number of entities to process in a single batch during initial state/info sending
+static constexpr size_t MAX_INITIAL_PER_BATCH = 20;
 
 class APIConnection : public APIServerConnection {
  public:
@@ -296,6 +298,20 @@ class APIConnection : public APIServerConnection {
   static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn,
                                            uint32_t remaining_size, bool is_single);
 
+  // Helper method to process multiple entities from an iterator in a batch
+  template void process_iterator_batch_(Iterator &iterator) {
+    size_t initial_size = this->deferred_batch_.size();
+    while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < MAX_INITIAL_PER_BATCH) {
+      iterator.advance();
+    }
+
+    // If the batch is full, process it immediately
+    // Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
+    if (this->deferred_batch_.size() >= MAX_INITIAL_PER_BATCH) {
+      this->process_batch_();
+    }
+  }
+
 #ifdef USE_BINARY_SENSOR
   static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
                                                bool is_single);
@@ -582,7 +598,8 @@ class APIConnection : public APIServerConnection {
     uint8_t service_call_subscription : 1;
     uint8_t next_close : 1;
     uint8_t batch_scheduled : 1;
-    uint8_t batch_first_message : 1;  // For batch buffer allocation
+    uint8_t batch_first_message : 1;          // For batch buffer allocation
+    uint8_t should_try_send_immediately : 1;  // True after initial states are sent
 #ifdef HAS_PROTO_MESSAGE_DUMP
     uint8_t log_only_mode : 1;
 #endif
@@ -609,11 +626,50 @@ class APIConnection : public APIServerConnection {
 
   bool schedule_batch_();
   void process_batch_();
+  void clear_batch_() {
+    this->deferred_batch_.clear();
+    this->flags_.batch_scheduled = false;
+  }
 
 #ifdef HAS_PROTO_MESSAGE_DUMP
-  void log_batch_item_(const DeferredBatch::BatchItem &item);
+  // Helper to log a proto message from a MessageCreator object
+  void log_proto_message_(EntityBase *entity, const MessageCreator &creator, uint16_t message_type) {
+    this->flags_.log_only_mode = true;
+    creator(entity, this, MAX_PACKET_SIZE, true, message_type);
+    this->flags_.log_only_mode = false;
+  }
+
+  void log_batch_item_(const DeferredBatch::BatchItem &item) {
+    // Use the helper to log the message
+    this->log_proto_message_(item.entity, item.creator, item.message_type);
+  }
 #endif
 
+  // Helper method to send a message either immediately or via batching
+  bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint16_t message_type) {
+    // Try to send immediately if:
+    // 1. We should try to send immediately (should_try_send_immediately = true)
+    // 2. Batch delay is 0 (user has opted in to immediate sending)
+    // 3. Buffer has space available
+    if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 &&
+        this->helper_->can_write_without_blocking()) {
+      // Now actually encode and send
+      if (creator(entity, this, MAX_PACKET_SIZE, true) &&
+          this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
+#ifdef HAS_PROTO_MESSAGE_DUMP
+        // Log the message in verbose mode
+        this->log_proto_message_(entity, MessageCreator(creator), message_type);
+#endif
+        return true;
+      }
+
+      // If immediate send failed, fall through to batching
+    }
+
+    // Fall back to scheduled batching
+    return this->schedule_message_(entity, creator, message_type);
+  }
+
   // Helper function to schedule a deferred message with known message type
   bool schedule_message_(EntityBase *entity, MessageCreator creator, uint16_t message_type) {
     this->deferred_batch_.add_item(entity, std::move(creator), message_type);
diff --git a/tests/integration/fixtures/batch_delay_zero_rapid_transitions.yaml b/tests/integration/fixtures/batch_delay_zero_rapid_transitions.yaml
new file mode 100644
index 0000000000..32cacfaa79
--- /dev/null
+++ b/tests/integration/fixtures/batch_delay_zero_rapid_transitions.yaml
@@ -0,0 +1,43 @@
+esphome:
+  name: rapid-transitions-test
+host:
+api:
+  batch_delay: 0ms  # Enable immediate sending for rapid transitions
+logger:
+  level: DEBUG
+
+# Add a sensor that updates frequently to trigger lambda evaluations
+sensor:
+  - platform: template
+    name: "Update Trigger"
+    id: update_trigger
+    lambda: |-
+      return 0;
+    update_interval: 10ms
+    internal: true
+
+# Simulate an IR remote binary sensor with rapid ON/OFF transitions
+binary_sensor:
+  - platform: template
+    name: "Simulated IR Remote Button"
+    id: ir_remote_button
+    lambda: |-
+      // Simulate rapid button presses every ~100ms
+      // Each "press" is ON for ~30ms then OFF
+      uint32_t now = millis();
+      uint32_t press_cycle = now % 100;  // 100ms cycle
+
+      // ON for first 30ms of each cycle
+      if (press_cycle < 30) {
+        // Only log state change
+        if (!id(ir_remote_button).state) {
+          ESP_LOGD("test", "Button ON at %u", now);
+        }
+        return true;
+      } else {
+        // Only log state change
+        if (id(ir_remote_button).state) {
+          ESP_LOGD("test", "Button OFF at %u", now);
+        }
+        return false;
+      }
diff --git a/tests/integration/test_batch_delay_zero_rapid_transitions.py b/tests/integration/test_batch_delay_zero_rapid_transitions.py
new file mode 100644
index 0000000000..f17319dddf
--- /dev/null
+++ b/tests/integration/test_batch_delay_zero_rapid_transitions.py
@@ -0,0 +1,58 @@
+"""Integration test for API batch_delay: 0 with rapid state transitions."""
+
+from __future__ import annotations
+
+import asyncio
+import time
+
+from aioesphomeapi import BinarySensorInfo, BinarySensorState, EntityState
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_batch_delay_zero_rapid_transitions(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that rapid binary sensor transitions are preserved with batch_delay: 0ms."""
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Track state changes
+        state_changes: list[tuple[bool, float]] = []
+
+        def on_state(state: EntityState) -> None:
+            """Track state changes with timestamps."""
+            if isinstance(state, BinarySensorState):
+                state_changes.append((state.state, time.monotonic()))
+
+        # Subscribe to state changes
+        client.subscribe_states(on_state)
+
+        # Wait for entity info
+        entity_info, _ = await client.list_entities_services()
+        binary_sensors = [e for e in entity_info if isinstance(e, BinarySensorInfo)]
+        assert len(binary_sensors) == 1, "Expected 1 binary sensor"
+
+        # Collect states for 2 seconds
+        await asyncio.sleep(2.1)
+
+        # Count ON->OFF transitions
+        on_off_count = 0
+        for i in range(1, len(state_changes)):
+            if state_changes[i - 1][0] and not state_changes[i][0]:  # ON to OFF
+                on_off_count += 1
+
+        # With batch_delay: 0, we should capture rapid transitions
+        # The test timing can be variable in CI, so we're being conservative
+        # We mainly want to verify that we capture multiple rapid transitions
+        assert on_off_count >= 5, (
+            f"Expected at least 5 ON->OFF transitions with batch_delay: 0ms, got {on_off_count}. "
+            "Rapid transitions may have been lost."
+        )
+
+        # Also verify that state changes are happening frequently
+        assert len(state_changes) >= 10, (
+            f"Expected at least 10 state changes, got {len(state_changes)}"
+        )
diff --git a/tests/integration/test_host_mode_empty_string_options.py b/tests/integration/test_host_mode_empty_string_options.py
index d2df839a75..16399dcfb8 100644
--- a/tests/integration/test_host_mode_empty_string_options.py
+++ b/tests/integration/test_host_mode_empty_string_options.py
@@ -74,37 +74,41 @@ async def test_host_mode_empty_string_options(
         # If we got here without protobuf decoding errors, the fix is working
         # The bug would have caused "Invalid protobuf message" errors with trailing bytes
 
-        # Also verify we can interact with the select entities
-        # Subscribe to state changes
+        # Also verify we can receive state updates for select entities
+        # This ensures empty strings work properly in state messages too
         states: dict[int, EntityState] = {}
-        state_change_future: asyncio.Future[None] = loop.create_future()
+        states_received_future: asyncio.Future[None] = loop.create_future()
+        expected_select_keys = {empty_first.key, empty_middle.key, empty_last.key}
+        received_select_keys = set()
 
         def on_state(state: EntityState) -> None:
             """Track state changes."""
             states[state.key] = state
-            # When we receive the state change for our select, resolve the future
-            if state.key == empty_first.key and not state_change_future.done():
-                state_change_future.set_result(None)
+            # Track which select entities we've received states for
+            if state.key in expected_select_keys:
+                received_select_keys.add(state.key)
+                # Once we have all select states, we're done
+                if (
+                    received_select_keys == expected_select_keys
+                    and not states_received_future.done()
+                ):
+                    states_received_future.set_result(None)
 
         client.subscribe_states(on_state)
 
-        # Try setting a select to an empty string option
-        # This further tests that empty strings are handled correctly
-        client.select_command(empty_first.key, "")
-
-        # Wait for state update with timeout
+        # Wait for initial states with timeout
         try:
-            await asyncio.wait_for(state_change_future, timeout=5.0)
+            await asyncio.wait_for(states_received_future, timeout=5.0)
         except asyncio.TimeoutError:
             pytest.fail(
-                "Did not receive state update after setting select to empty string"
+                f"Did not receive states for all select entities. "
+                f"Expected keys: {expected_select_keys}, Received: {received_select_keys}"
             )
 
-        # Verify the state was set to empty string
+        # Verify we received states for all select entities
         assert empty_first.key in states
-        select_state = states[empty_first.key]
-        assert hasattr(select_state, "state")
-        assert select_state.state == ""
+        assert empty_middle.key in states
+        assert empty_last.key in states
 
-        # The test passes if no protobuf decoding errors occurred
-        # With the bug, we would have gotten "Invalid protobuf message" errors
+        # The main test is that we got here without protobuf errors
+        # The select entities with empty string options were properly encoded
diff --git a/tests/integration/test_host_mode_fan_preset.py b/tests/integration/test_host_mode_fan_preset.py
index 1d956a7290..d18b9f08ad 100644
--- a/tests/integration/test_host_mode_fan_preset.py
+++ b/tests/integration/test_host_mode_fan_preset.py
@@ -46,14 +46,22 @@ async def test_host_mode_fan_preset(
         # Subscribe to states
         states: dict[int, FanState] = {}
         state_event = asyncio.Event()
+        initial_states_received = set()
 
         def on_state(state: FanState) -> None:
             if isinstance(state, FanState):
                 states[state.key] = state
+                initial_states_received.add(state.key)
                 state_event.set()
 
         client.subscribe_states(on_state)
 
+        # Wait for initial states to be received for all fans
+        expected_fan_keys = {fan.key for fan in fans}
+        while initial_states_received != expected_fan_keys:
+            state_event.clear()
+            await asyncio.wait_for(state_event.wait(), timeout=2.0)
+
         # Test 1: Turn on fan without speed or preset - should set speed to 100%
         state_event.clear()
         client.fan_command(
diff --git a/tests/integration/test_host_mode_many_entities.py b/tests/integration/test_host_mode_many_entities.py
index d5622e6fa4..005728b8c6 100644
--- a/tests/integration/test_host_mode_many_entities.py
+++ b/tests/integration/test_host_mode_many_entities.py
@@ -22,36 +22,51 @@ async def test_host_mode_many_entities(
     async with run_compiled(yaml_config), api_client_connected() as client:
         # Subscribe to state changes
         states: dict[int, EntityState] = {}
-        entity_count_future: asyncio.Future[int] = loop.create_future()
+        sensor_count_future: asyncio.Future[int] = loop.create_future()
 
         def on_state(state: EntityState) -> None:
             states[state.key] = state
-            # When we have received states from a good number of entities, resolve the future
-            if len(states) >= 50 and not entity_count_future.done():
-                entity_count_future.set_result(len(states))
+            # Count sensor states specifically
+            sensor_states = [
+                s
+                for s in states.values()
+                if hasattr(s, "state") and isinstance(s.state, float)
+            ]
+            # When we have received states from at least 50 sensors, resolve the future
+            if len(sensor_states) >= 50 and not sensor_count_future.done():
+                sensor_count_future.set_result(len(sensor_states))
 
         client.subscribe_states(on_state)
 
-        # Wait for states from at least 50 entities with timeout
+        # Wait for states from at least 50 sensors with timeout
         try:
-            entity_count = await asyncio.wait_for(entity_count_future, timeout=10.0)
+            sensor_count = await asyncio.wait_for(sensor_count_future, timeout=10.0)
         except asyncio.TimeoutError:
+            sensor_states = [
+                s
+                for s in states.values()
+                if hasattr(s, "state") and isinstance(s.state, float)
+            ]
             pytest.fail(
-                f"Did not receive states from at least 50 entities within 10 seconds. "
-                f"Received {len(states)} states: {list(states.keys())}"
+                f"Did not receive states from at least 50 sensors within 10 seconds. "
+                f"Received {len(sensor_states)} sensor states out of {len(states)} total states"
             )
 
         # Verify we received a good number of entity states
-        assert entity_count >= 50, f"Expected at least 50 entities, got {entity_count}"
-        assert len(states) >= 50, f"Expected at least 50 states, got {len(states)}"
+        assert len(states) >= 50, (
+            f"Expected at least 50 total states, got {len(states)}"
+        )
 
-        # Verify we have different entity types by checking some expected values
+        # Verify we have the expected sensor states
         sensor_states = [
             s
             for s in states.values()
             if hasattr(s, "state") and isinstance(s.state, float)
         ]
 
+        assert sensor_count >= 50, (
+            f"Expected at least 50 sensor states, got {sensor_count}"
+        )
         assert len(sensor_states) >= 50, (
             f"Expected at least 50 sensor states, got {len(sensor_states)}"
         )

From b2b6f41ef3480530ed54aa3b8e68574b92f1e700 Mon Sep 17 00:00:00 2001
From: Sergey Dudanov 
Date: Thu, 3 Jul 2025 11:11:40 +0400
Subject: [PATCH 221/293] Packages: optional base path for remote git packages
 (#9279)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/packages/__init__.py       | 4 ++++
 tests/components/packages/test.esp32-ard.yaml | 3 ++-
 tests/components/packages/test.esp32-idf.yaml | 3 ++-
 3 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py
index 6eb746ec63..0db7841db2 100644
--- a/esphome/components/packages/__init__.py
+++ b/esphome/components/packages/__init__.py
@@ -63,6 +63,7 @@ BASE_SCHEMA = cv.All(
     cv.Schema(
         {
             cv.Required(CONF_URL): cv.url,
+            cv.Optional(CONF_PATH): cv.string,
             cv.Optional(CONF_USERNAME): cv.string,
             cv.Optional(CONF_PASSWORD): cv.string,
             cv.Exclusive(CONF_FILE, CONF_FILES): validate_yaml_filename,
@@ -116,6 +117,9 @@ def _process_base_package(config: dict) -> dict:
     )
     files = []
 
+    if base_path := config.get(CONF_PATH):
+        repo_dir = repo_dir / base_path
+
     for file in config[CONF_FILES]:
         if isinstance(file, str):
             files.append({CONF_PATH: file, CONF_VARS: {}})
diff --git a/tests/components/packages/test.esp32-ard.yaml b/tests/components/packages/test.esp32-ard.yaml
index d882116c10..7d0ab2b905 100644
--- a/tests/components/packages/test.esp32-ard.yaml
+++ b/tests/components/packages/test.esp32-ard.yaml
@@ -5,7 +5,8 @@ packages:
   - !include package.yaml
   - github://esphome/esphome/tests/components/template/common.yaml@dev
   - url: https://github.com/esphome/esphome
-    file: tests/components/absolute_humidity/common.yaml
+    path: tests/components/absolute_humidity
+    file: common.yaml
     ref: dev
     refresh: 1d
 
diff --git a/tests/components/packages/test.esp32-idf.yaml b/tests/components/packages/test.esp32-idf.yaml
index 720a5777c2..8c0a34bb1a 100644
--- a/tests/components/packages/test.esp32-idf.yaml
+++ b/tests/components/packages/test.esp32-idf.yaml
@@ -7,7 +7,8 @@ packages:
   shorthand: github://esphome/esphome/tests/components/template/common.yaml@dev
   github:
     url: https://github.com/esphome/esphome
-    file: tests/components/absolute_humidity/common.yaml
+    path: tests/components/absolute_humidity
+    file: common.yaml
     ref: dev
     refresh: 1d
 

From 107304b274417ab7005a0bebcb29e6ab3a0ab3c3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 3 Jul 2025 14:08:04 +0000
Subject: [PATCH 222/293] Bump aioesphomeapi from 34.0.0 to 34.1.0 (#9301)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 1010a311d6..c4dae9792d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,7 +13,7 @@ platformio==6.1.18  # When updating platformio, also update /docker/Dockerfile
 esptool==4.9.0
 click==8.1.7
 esphome-dashboard==20250514.0
-aioesphomeapi==34.0.0
+aioesphomeapi==34.1.0
 zeroconf==0.147.0
 puremagic==1.29
 ruamel.yaml==0.18.14 # dashboard_import

From 1ef7b2d64f5a7e8fad2aac63a3be15ff69b9b9a8 Mon Sep 17 00:00:00 2001
From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Date: Thu, 3 Jul 2025 11:37:18 -0400
Subject: [PATCH 223/293] [sx127x] Add sx127x component (#7490)

Co-authored-by: Jonathan Swoboda 
---
 CODEOWNERS                                    |   1 +
 esphome/components/sx127x/__init__.py         | 325 ++++++++++++
 esphome/components/sx127x/automation.h        |  62 +++
 .../sx127x/packet_transport/__init__.py       |  26 +
 .../packet_transport/sx127x_transport.cpp     |  26 +
 .../packet_transport/sx127x_transport.h       |  25 +
 esphome/components/sx127x/sx127x.cpp          | 493 ++++++++++++++++++
 esphome/components/sx127x/sx127x.h            | 125 +++++
 esphome/components/sx127x/sx127x_reg.h        | 295 +++++++++++
 tests/components/sx127x/common.yaml           |  45 ++
 tests/components/sx127x/test.esp32-ard.yaml   |   9 +
 .../components/sx127x/test.esp32-c3-ard.yaml  |   9 +
 .../components/sx127x/test.esp32-c3-idf.yaml  |   9 +
 tests/components/sx127x/test.esp32-idf.yaml   |   9 +
 tests/components/sx127x/test.esp8266-ard.yaml |   9 +
 tests/components/sx127x/test.rp2040-ard.yaml  |   9 +
 16 files changed, 1477 insertions(+)
 create mode 100644 esphome/components/sx127x/__init__.py
 create mode 100644 esphome/components/sx127x/automation.h
 create mode 100644 esphome/components/sx127x/packet_transport/__init__.py
 create mode 100644 esphome/components/sx127x/packet_transport/sx127x_transport.cpp
 create mode 100644 esphome/components/sx127x/packet_transport/sx127x_transport.h
 create mode 100644 esphome/components/sx127x/sx127x.cpp
 create mode 100644 esphome/components/sx127x/sx127x.h
 create mode 100644 esphome/components/sx127x/sx127x_reg.h
 create mode 100644 tests/components/sx127x/common.yaml
 create mode 100644 tests/components/sx127x/test.esp32-ard.yaml
 create mode 100644 tests/components/sx127x/test.esp32-c3-ard.yaml
 create mode 100644 tests/components/sx127x/test.esp32-c3-idf.yaml
 create mode 100644 tests/components/sx127x/test.esp32-idf.yaml
 create mode 100644 tests/components/sx127x/test.esp8266-ard.yaml
 create mode 100644 tests/components/sx127x/test.rp2040-ard.yaml

diff --git a/CODEOWNERS b/CODEOWNERS
index 295dd9b1b2..540f33853d 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -441,6 +441,7 @@ esphome/components/sun/* @OttoWinter
 esphome/components/sun_gtil2/* @Mat931
 esphome/components/switch/* @esphome/core
 esphome/components/switch/binary_sensor/* @ssieb
+esphome/components/sx127x/* @swoboda1337
 esphome/components/syslog/* @clydebarrow
 esphome/components/t6615/* @tylermenezes
 esphome/components/tc74/* @sethgirvan
diff --git a/esphome/components/sx127x/__init__.py b/esphome/components/sx127x/__init__.py
new file mode 100644
index 0000000000..4d034801cc
--- /dev/null
+++ b/esphome/components/sx127x/__init__.py
@@ -0,0 +1,325 @@
+from esphome import automation, pins
+import esphome.codegen as cg
+from esphome.components import spi
+import esphome.config_validation as cv
+from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID
+
+MULTI_CONF = True
+CODEOWNERS = ["@swoboda1337"]
+DEPENDENCIES = ["spi"]
+
+CONF_SX127X_ID = "sx127x_id"
+
+CONF_AUTO_CAL = "auto_cal"
+CONF_BANDWIDTH = "bandwidth"
+CONF_BITRATE = "bitrate"
+CONF_BITSYNC = "bitsync"
+CONF_CODING_RATE = "coding_rate"
+CONF_CRC_ENABLE = "crc_enable"
+CONF_DEVIATION = "deviation"
+CONF_DIO0_PIN = "dio0_pin"
+CONF_MODULATION = "modulation"
+CONF_ON_PACKET = "on_packet"
+CONF_PA_PIN = "pa_pin"
+CONF_PA_POWER = "pa_power"
+CONF_PA_RAMP = "pa_ramp"
+CONF_PACKET_MODE = "packet_mode"
+CONF_PAYLOAD_LENGTH = "payload_length"
+CONF_PREAMBLE_DETECT = "preamble_detect"
+CONF_PREAMBLE_ERRORS = "preamble_errors"
+CONF_PREAMBLE_POLARITY = "preamble_polarity"
+CONF_PREAMBLE_SIZE = "preamble_size"
+CONF_RST_PIN = "rst_pin"
+CONF_RX_FLOOR = "rx_floor"
+CONF_RX_START = "rx_start"
+CONF_SHAPING = "shaping"
+CONF_SPREADING_FACTOR = "spreading_factor"
+CONF_SYNC_VALUE = "sync_value"
+
+sx127x_ns = cg.esphome_ns.namespace("sx127x")
+SX127x = sx127x_ns.class_("SX127x", cg.Component, spi.SPIDevice)
+SX127xListener = sx127x_ns.class_("SX127xListener")
+SX127xBw = sx127x_ns.enum("SX127xBw")
+SX127xOpMode = sx127x_ns.enum("SX127xOpMode")
+SX127xPaConfig = sx127x_ns.enum("SX127xPaConfig")
+SX127xPaRamp = sx127x_ns.enum("SX127xPaRamp")
+SX127xModemCfg1 = sx127x_ns.enum("SX127xModemCfg1")
+
+BW = {
+    "2_6kHz": SX127xBw.SX127X_BW_2_6,
+    "3_1kHz": SX127xBw.SX127X_BW_3_1,
+    "3_9kHz": SX127xBw.SX127X_BW_3_9,
+    "5_2kHz": SX127xBw.SX127X_BW_5_2,
+    "6_3kHz": SX127xBw.SX127X_BW_6_3,
+    "7_8kHz": SX127xBw.SX127X_BW_7_8,
+    "10_4kHz": SX127xBw.SX127X_BW_10_4,
+    "12_5kHz": SX127xBw.SX127X_BW_12_5,
+    "15_6kHz": SX127xBw.SX127X_BW_15_6,
+    "20_8kHz": SX127xBw.SX127X_BW_20_8,
+    "25_0kHz": SX127xBw.SX127X_BW_25_0,
+    "31_3kHz": SX127xBw.SX127X_BW_31_3,
+    "41_7kHz": SX127xBw.SX127X_BW_41_7,
+    "50_0kHz": SX127xBw.SX127X_BW_50_0,
+    "62_5kHz": SX127xBw.SX127X_BW_62_5,
+    "83_3kHz": SX127xBw.SX127X_BW_83_3,
+    "100_0kHz": SX127xBw.SX127X_BW_100_0,
+    "125_0kHz": SX127xBw.SX127X_BW_125_0,
+    "166_7kHz": SX127xBw.SX127X_BW_166_7,
+    "200_0kHz": SX127xBw.SX127X_BW_200_0,
+    "250_0kHz": SX127xBw.SX127X_BW_250_0,
+    "500_0kHz": SX127xBw.SX127X_BW_500_0,
+}
+
+CODING_RATE = {
+    "CR_4_5": SX127xModemCfg1.CODING_RATE_4_5,
+    "CR_4_6": SX127xModemCfg1.CODING_RATE_4_6,
+    "CR_4_7": SX127xModemCfg1.CODING_RATE_4_7,
+    "CR_4_8": SX127xModemCfg1.CODING_RATE_4_8,
+}
+
+MOD = {
+    "LORA": SX127xOpMode.MOD_LORA,
+    "FSK": SX127xOpMode.MOD_FSK,
+    "OOK": SX127xOpMode.MOD_OOK,
+}
+
+PA_PIN = {
+    "RFO": SX127xPaConfig.PA_PIN_RFO,
+    "BOOST": SX127xPaConfig.PA_PIN_BOOST,
+}
+
+RAMP = {
+    "10us": SX127xPaRamp.PA_RAMP_10,
+    "12us": SX127xPaRamp.PA_RAMP_12,
+    "15us": SX127xPaRamp.PA_RAMP_15,
+    "20us": SX127xPaRamp.PA_RAMP_20,
+    "25us": SX127xPaRamp.PA_RAMP_25,
+    "31us": SX127xPaRamp.PA_RAMP_31,
+    "40us": SX127xPaRamp.PA_RAMP_40,
+    "50us": SX127xPaRamp.PA_RAMP_50,
+    "62us": SX127xPaRamp.PA_RAMP_62,
+    "100us": SX127xPaRamp.PA_RAMP_100,
+    "125us": SX127xPaRamp.PA_RAMP_125,
+    "250us": SX127xPaRamp.PA_RAMP_250,
+    "500us": SX127xPaRamp.PA_RAMP_500,
+    "1000us": SX127xPaRamp.PA_RAMP_1000,
+    "2000us": SX127xPaRamp.PA_RAMP_2000,
+    "3400us": SX127xPaRamp.PA_RAMP_3400,
+}
+
+SHAPING = {
+    "CUTOFF_BR_X_2": SX127xPaRamp.CUTOFF_BR_X_2,
+    "CUTOFF_BR_X_1": SX127xPaRamp.CUTOFF_BR_X_1,
+    "GAUSSIAN_BT_0_3": SX127xPaRamp.GAUSSIAN_BT_0_3,
+    "GAUSSIAN_BT_0_5": SX127xPaRamp.GAUSSIAN_BT_0_5,
+    "GAUSSIAN_BT_1_0": SX127xPaRamp.GAUSSIAN_BT_1_0,
+    "NONE": SX127xPaRamp.SHAPING_NONE,
+}
+
+RunImageCalAction = sx127x_ns.class_(
+    "RunImageCalAction", automation.Action, cg.Parented.template(SX127x)
+)
+SendPacketAction = sx127x_ns.class_(
+    "SendPacketAction", automation.Action, cg.Parented.template(SX127x)
+)
+SetModeTxAction = sx127x_ns.class_(
+    "SetModeTxAction", automation.Action, cg.Parented.template(SX127x)
+)
+SetModeRxAction = sx127x_ns.class_(
+    "SetModeRxAction", automation.Action, cg.Parented.template(SX127x)
+)
+SetModeSleepAction = sx127x_ns.class_(
+    "SetModeSleepAction", automation.Action, cg.Parented.template(SX127x)
+)
+SetModeStandbyAction = sx127x_ns.class_(
+    "SetModeStandbyAction", automation.Action, cg.Parented.template(SX127x)
+)
+
+
+def validate_raw_data(value):
+    if isinstance(value, str):
+        return value.encode("utf-8")
+    if isinstance(value, list):
+        return cv.Schema([cv.hex_uint8_t])(value)
+    raise cv.Invalid(
+        "data must either be a string wrapped in quotes or a list of bytes"
+    )
+
+
+def validate_config(config):
+    if config[CONF_MODULATION] == "LORA":
+        bws = [
+            "7_8kHz",
+            "10_4kHz",
+            "15_6kHz",
+            "20_8kHz",
+            "31_3kHz",
+            "41_7kHz",
+            "62_5kHz",
+            "125_0kHz",
+            "250_0kHz",
+            "500_0kHz",
+        ]
+        if config[CONF_BANDWIDTH] not in bws:
+            raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with LORA")
+        if CONF_DIO0_PIN not in config:
+            raise cv.Invalid("Cannot use LoRa without dio0_pin")
+        if 0 < config[CONF_PREAMBLE_SIZE] < 6:
+            raise cv.Invalid("Minimum preamble size is 6 with LORA")
+        if config[CONF_SPREADING_FACTOR] == 6 and config[CONF_PAYLOAD_LENGTH] == 0:
+            raise cv.Invalid("Payload length must be set when spreading factor is 6")
+    else:
+        if config[CONF_BANDWIDTH] == "500_0kHz":
+            raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is only available with LORA")
+        if CONF_BITSYNC not in config:
+            raise cv.Invalid("Config 'bitsync' required with FSK/OOK")
+        if CONF_PACKET_MODE not in config:
+            raise cv.Invalid("Config 'packet_mode' required with FSK/OOK")
+        if config[CONF_PACKET_MODE] and CONF_DIO0_PIN not in config:
+            raise cv.Invalid("Config 'dio0_pin' required in packet mode")
+        if config[CONF_PAYLOAD_LENGTH] > 64:
+            raise cv.Invalid("Payload length must be <= 64 with FSK/OOK")
+    if config[CONF_PA_PIN] == "RFO" and config[CONF_PA_POWER] > 15:
+        raise cv.Invalid("PA power must be <= 15 dbm when using the RFO pin")
+    if config[CONF_PA_PIN] == "BOOST" and config[CONF_PA_POWER] < 2:
+        raise cv.Invalid("PA power must be >= 2 dbm when using the BOOST pin")
+    return config
+
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(SX127x),
+            cv.Optional(CONF_AUTO_CAL, default=True): cv.boolean,
+            cv.Optional(CONF_BANDWIDTH, default="125_0kHz"): cv.enum(BW),
+            cv.Optional(CONF_BITRATE, default=4800): cv.int_range(min=500, max=300000),
+            cv.Optional(CONF_BITSYNC): cv.boolean,
+            cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE),
+            cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
+            cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000),
+            cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema,
+            cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000),
+            cv.Required(CONF_MODULATION): cv.enum(MOD),
+            cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
+            cv.Optional(CONF_PA_PIN, default="BOOST"): cv.enum(PA_PIN),
+            cv.Optional(CONF_PA_POWER, default=17): cv.int_range(min=0, max=17),
+            cv.Optional(CONF_PA_RAMP, default="40us"): cv.enum(RAMP),
+            cv.Optional(CONF_PACKET_MODE): cv.boolean,
+            cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=256),
+            cv.Optional(CONF_PREAMBLE_DETECT, default=0): cv.int_range(min=0, max=3),
+            cv.Optional(CONF_PREAMBLE_ERRORS, default=0): cv.int_range(min=0, max=31),
+            cv.Optional(CONF_PREAMBLE_POLARITY, default=0xAA): cv.All(
+                cv.hex_int, cv.one_of(0xAA, 0x55)
+            ),
+            cv.Optional(CONF_PREAMBLE_SIZE, default=0): cv.int_range(min=0, max=65535),
+            cv.Required(CONF_RST_PIN): pins.internal_gpio_output_pin_schema,
+            cv.Optional(CONF_RX_FLOOR, default=-94): cv.float_range(min=-128, max=-1),
+            cv.Optional(CONF_RX_START, default=True): cv.boolean,
+            cv.Optional(CONF_SHAPING, default="NONE"): cv.enum(SHAPING),
+            cv.Optional(CONF_SPREADING_FACTOR, default=7): cv.int_range(min=6, max=12),
+            cv.Optional(CONF_SYNC_VALUE, default=[]): cv.ensure_list(cv.hex_uint8_t),
+        },
+    )
+    .extend(cv.COMPONENT_SCHEMA)
+    .extend(spi.spi_device_schema(True, 8e6, "mode0"))
+    .add_extra(validate_config)
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await spi.register_spi_device(var, config)
+    if CONF_ON_PACKET in config:
+        await automation.build_automation(
+            var.get_packet_trigger(),
+            [
+                (cg.std_vector.template(cg.uint8), "x"),
+                (cg.float_, "rssi"),
+                (cg.float_, "snr"),
+            ],
+            config[CONF_ON_PACKET],
+        )
+    if CONF_DIO0_PIN in config:
+        dio0_pin = await cg.gpio_pin_expression(config[CONF_DIO0_PIN])
+        cg.add(var.set_dio0_pin(dio0_pin))
+    rst_pin = await cg.gpio_pin_expression(config[CONF_RST_PIN])
+    cg.add(var.set_rst_pin(rst_pin))
+    cg.add(var.set_auto_cal(config[CONF_AUTO_CAL]))
+    cg.add(var.set_bandwidth(config[CONF_BANDWIDTH]))
+    cg.add(var.set_frequency(config[CONF_FREQUENCY]))
+    cg.add(var.set_deviation(config[CONF_DEVIATION]))
+    cg.add(var.set_modulation(config[CONF_MODULATION]))
+    if config[CONF_MODULATION] != "LORA":
+        cg.add(var.set_bitrate(config[CONF_BITRATE]))
+        cg.add(var.set_bitsync(config[CONF_BITSYNC]))
+        cg.add(var.set_packet_mode(config[CONF_PACKET_MODE]))
+    cg.add(var.set_pa_pin(config[CONF_PA_PIN]))
+    cg.add(var.set_pa_ramp(config[CONF_PA_RAMP]))
+    cg.add(var.set_pa_power(config[CONF_PA_POWER]))
+    cg.add(var.set_shaping(config[CONF_SHAPING]))
+    cg.add(var.set_crc_enable(config[CONF_CRC_ENABLE]))
+    cg.add(var.set_payload_length(config[CONF_PAYLOAD_LENGTH]))
+    cg.add(var.set_preamble_detect(config[CONF_PREAMBLE_DETECT]))
+    cg.add(var.set_preamble_size(config[CONF_PREAMBLE_SIZE]))
+    cg.add(var.set_preamble_polarity(config[CONF_PREAMBLE_POLARITY]))
+    cg.add(var.set_preamble_errors(config[CONF_PREAMBLE_ERRORS]))
+    cg.add(var.set_coding_rate(config[CONF_CODING_RATE]))
+    cg.add(var.set_spreading_factor(config[CONF_SPREADING_FACTOR]))
+    cg.add(var.set_sync_value(config[CONF_SYNC_VALUE]))
+    cg.add(var.set_rx_floor(config[CONF_RX_FLOOR]))
+    cg.add(var.set_rx_start(config[CONF_RX_START]))
+
+
+NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id(
+    {
+        cv.GenerateID(): cv.use_id(SX127x),
+    }
+)
+
+
+@automation.register_action(
+    "sx127x.run_image_cal", RunImageCalAction, NO_ARGS_ACTION_SCHEMA
+)
+@automation.register_action(
+    "sx127x.set_mode_tx", SetModeTxAction, NO_ARGS_ACTION_SCHEMA
+)
+@automation.register_action(
+    "sx127x.set_mode_rx", SetModeRxAction, NO_ARGS_ACTION_SCHEMA
+)
+@automation.register_action(
+    "sx127x.set_mode_sleep", SetModeSleepAction, NO_ARGS_ACTION_SCHEMA
+)
+@automation.register_action(
+    "sx127x.set_mode_standby", SetModeStandbyAction, NO_ARGS_ACTION_SCHEMA
+)
+async def no_args_action_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    await cg.register_parented(var, config[CONF_ID])
+    return var
+
+
+SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
+    {
+        cv.GenerateID(): cv.use_id(SX127x),
+        cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
+    },
+    key=CONF_DATA,
+)
+
+
+@automation.register_action(
+    "sx127x.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA
+)
+async def send_packet_action_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    await cg.register_parented(var, config[CONF_ID])
+    data = config[CONF_DATA]
+    if isinstance(data, bytes):
+        data = list(data)
+    if cg.is_template(data):
+        templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
+        cg.add(var.set_data_template(templ))
+    else:
+        cg.add(var.set_data_static(data))
+    return var
diff --git a/esphome/components/sx127x/automation.h b/esphome/components/sx127x/automation.h
new file mode 100644
index 0000000000..2b9c261de1
--- /dev/null
+++ b/esphome/components/sx127x/automation.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/automation.h"
+#include "esphome/components/sx127x/sx127x.h"
+
+namespace esphome {
+namespace sx127x {
+
+template class RunImageCalAction : public Action, public Parented {
+ public:
+  void play(Ts... x) override { this->parent_->run_image_cal(); }
+};
+
+template class SendPacketAction : public Action, public Parented {
+ public:
+  void set_data_template(std::function(Ts...)> func) {
+    this->data_func_ = func;
+    this->static_ = false;
+  }
+
+  void set_data_static(const std::vector &data) {
+    this->data_static_ = data;
+    this->static_ = true;
+  }
+
+  void play(Ts... x) override {
+    if (this->static_) {
+      this->parent_->transmit_packet(this->data_static_);
+    } else {
+      this->parent_->transmit_packet(this->data_func_(x...));
+    }
+  }
+
+ protected:
+  bool static_{false};
+  std::function(Ts...)> data_func_{};
+  std::vector data_static_{};
+};
+
+template class SetModeTxAction : public Action, public Parented {
+ public:
+  void play(Ts... x) override { this->parent_->set_mode_tx(); }
+};
+
+template class SetModeRxAction : public Action, public Parented {
+ public:
+  void play(Ts... x) override { this->parent_->set_mode_rx(); }
+};
+
+template class SetModeSleepAction : public Action, public Parented {
+ public:
+  void play(Ts... x) override { this->parent_->set_mode_sleep(); }
+};
+
+template class SetModeStandbyAction : public Action, public Parented {
+ public:
+  void play(Ts... x) override { this->parent_->set_mode_standby(); }
+};
+
+}  // namespace sx127x
+}  // namespace esphome
diff --git a/esphome/components/sx127x/packet_transport/__init__.py b/esphome/components/sx127x/packet_transport/__init__.py
new file mode 100644
index 0000000000..2f3a0f6e2b
--- /dev/null
+++ b/esphome/components/sx127x/packet_transport/__init__.py
@@ -0,0 +1,26 @@
+import esphome.codegen as cg
+from esphome.components.packet_transport import (
+    PacketTransport,
+    new_packet_transport,
+    transport_schema,
+)
+import esphome.config_validation as cv
+from esphome.cpp_types import PollingComponent
+
+from .. import CONF_SX127X_ID, SX127x, SX127xListener, sx127x_ns
+
+SX127xTransport = sx127x_ns.class_(
+    "SX127xTransport", PacketTransport, PollingComponent, SX127xListener
+)
+
+CONFIG_SCHEMA = transport_schema(SX127xTransport).extend(
+    {
+        cv.GenerateID(CONF_SX127X_ID): cv.use_id(SX127x),
+    }
+)
+
+
+async def to_code(config):
+    var, _ = await new_packet_transport(config)
+    sx127x = await cg.get_variable(config[CONF_SX127X_ID])
+    cg.add(var.set_parent(sx127x))
diff --git a/esphome/components/sx127x/packet_transport/sx127x_transport.cpp b/esphome/components/sx127x/packet_transport/sx127x_transport.cpp
new file mode 100644
index 0000000000..b1d014bb96
--- /dev/null
+++ b/esphome/components/sx127x/packet_transport/sx127x_transport.cpp
@@ -0,0 +1,26 @@
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+#include "sx127x_transport.h"
+
+namespace esphome {
+namespace sx127x {
+
+static const char *const TAG = "sx127x_transport";
+
+void SX127xTransport::setup() {
+  PacketTransport::setup();
+  this->parent_->register_listener(this);
+}
+
+void SX127xTransport::update() {
+  PacketTransport::update();
+  this->updated_ = true;
+  this->resend_data_ = true;
+}
+
+void SX127xTransport::send_packet(const std::vector &buf) const { this->parent_->transmit_packet(buf); }
+
+void SX127xTransport::on_packet(const std::vector &packet, float rssi, float snr) { this->process_(packet); }
+
+}  // namespace sx127x
+}  // namespace esphome
diff --git a/esphome/components/sx127x/packet_transport/sx127x_transport.h b/esphome/components/sx127x/packet_transport/sx127x_transport.h
new file mode 100644
index 0000000000..e27b7f8d57
--- /dev/null
+++ b/esphome/components/sx127x/packet_transport/sx127x_transport.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sx127x/sx127x.h"
+#include "esphome/components/packet_transport/packet_transport.h"
+#include 
+
+namespace esphome {
+namespace sx127x {
+
+class SX127xTransport : public packet_transport::PacketTransport, public Parented, public SX127xListener {
+ public:
+  void setup() override;
+  void update() override;
+  void on_packet(const std::vector &packet, float rssi, float snr) override;
+  float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
+
+ protected:
+  void send_packet(const std::vector &buf) const override;
+  bool should_send() override { return true; }
+  size_t get_max_packet_size() override { return this->parent_->get_max_packet_size(); }
+};
+
+}  // namespace sx127x
+}  // namespace esphome
diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp
new file mode 100644
index 0000000000..e41efe098c
--- /dev/null
+++ b/esphome/components/sx127x/sx127x.cpp
@@ -0,0 +1,493 @@
+#include "sx127x.h"
+#include "esphome/core/hal.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace sx127x {
+
+static const char *const TAG = "sx127x";
+static const uint32_t FXOSC = 32000000u;
+static const uint16_t RAMP[16] = {3400, 2000, 1000, 500, 250, 125, 100, 62, 50, 40, 31, 25, 20, 15, 12, 10};
+static const uint32_t BW_HZ[22] = {2604,  3125,  3906,  5208,  6250,  7812,   10416,  12500,  15625,  20833,  25000,
+                                   31250, 41666, 50000, 62500, 83333, 100000, 125000, 166666, 200000, 250000, 500000};
+static const uint8_t BW_LORA[22] = {BW_7_8,   BW_7_8,   BW_7_8,   BW_7_8,   BW_7_8,   BW_7_8,  BW_10_4, BW_15_6,
+                                    BW_15_6,  BW_20_8,  BW_31_3,  BW_31_3,  BW_41_7,  BW_62_5, BW_62_5, BW_125_0,
+                                    BW_125_0, BW_125_0, BW_250_0, BW_250_0, BW_250_0, BW_500_0};
+static const uint8_t BW_FSK_OOK[22] = {RX_BW_2_6,   RX_BW_3_1,   RX_BW_3_9,   RX_BW_5_2,  RX_BW_6_3,   RX_BW_7_8,
+                                       RX_BW_10_4,  RX_BW_12_5,  RX_BW_15_6,  RX_BW_20_8, RX_BW_25_0,  RX_BW_31_3,
+                                       RX_BW_41_7,  RX_BW_50_0,  RX_BW_62_5,  RX_BW_83_3, RX_BW_100_0, RX_BW_125_0,
+                                       RX_BW_166_7, RX_BW_200_0, RX_BW_250_0, RX_BW_250_0};
+static const int32_t RSSI_OFFSET_HF = 157;
+static const int32_t RSSI_OFFSET_LF = 164;
+
+uint8_t SX127x::read_register_(uint8_t reg) {
+  this->enable();
+  this->write_byte(reg & 0x7F);
+  uint8_t value = this->read_byte();
+  this->disable();
+  return value;
+}
+
+void SX127x::write_register_(uint8_t reg, uint8_t value) {
+  this->enable();
+  this->write_byte(reg | 0x80);
+  this->write_byte(value);
+  this->disable();
+}
+
+void SX127x::read_fifo_(std::vector &packet) {
+  this->enable();
+  this->write_byte(REG_FIFO & 0x7F);
+  this->read_array(packet.data(), packet.size());
+  this->disable();
+}
+
+void SX127x::write_fifo_(const std::vector &packet) {
+  this->enable();
+  this->write_byte(REG_FIFO | 0x80);
+  this->write_array(packet.data(), packet.size());
+  this->disable();
+}
+
+void SX127x::setup() {
+  ESP_LOGCONFIG(TAG, "Running setup");
+
+  // setup reset
+  this->rst_pin_->setup();
+
+  // setup dio0
+  if (this->dio0_pin_) {
+    this->dio0_pin_->setup();
+  }
+
+  // start spi
+  this->spi_setup();
+
+  // configure rf
+  this->configure();
+}
+
+void SX127x::configure() {
+  // toggle chip reset
+  this->rst_pin_->digital_write(false);
+  delayMicroseconds(1000);
+  this->rst_pin_->digital_write(true);
+  delayMicroseconds(10000);
+
+  // check silicon version to make sure hw is ok
+  if (this->read_register_(REG_VERSION) != 0x12) {
+    this->mark_failed();
+    return;
+  }
+
+  // enter sleep mode
+  this->set_mode_(MOD_FSK, MODE_SLEEP);
+
+  // set freq
+  uint64_t frf = ((uint64_t) this->frequency_ << 19) / FXOSC;
+  this->write_register_(REG_FRF_MSB, (uint8_t) ((frf >> 16) & 0xFF));
+  this->write_register_(REG_FRF_MID, (uint8_t) ((frf >> 8) & 0xFF));
+  this->write_register_(REG_FRF_LSB, (uint8_t) ((frf >> 0) & 0xFF));
+
+  // enter standby mode
+  this->set_mode_(MOD_FSK, MODE_STDBY);
+
+  // run image cal
+  this->run_image_cal();
+
+  // go back to sleep
+  this->set_mode_sleep();
+
+  // config pa
+  if (this->pa_pin_ == PA_PIN_BOOST) {
+    this->pa_power_ = std::max(this->pa_power_, (uint8_t) 2);
+    this->pa_power_ = std::min(this->pa_power_, (uint8_t) 17);
+    this->write_register_(REG_PA_CONFIG, (this->pa_power_ - 2) | this->pa_pin_ | PA_MAX_POWER);
+  } else {
+    this->pa_power_ = std::min(this->pa_power_, (uint8_t) 14);
+    this->write_register_(REG_PA_CONFIG, (this->pa_power_ - 0) | this->pa_pin_ | PA_MAX_POWER);
+  }
+  if (this->modulation_ != MOD_LORA) {
+    this->write_register_(REG_PA_RAMP, this->pa_ramp_ | this->shaping_);
+  } else {
+    this->write_register_(REG_PA_RAMP, this->pa_ramp_);
+  }
+
+  // configure modem
+  if (this->modulation_ != MOD_LORA) {
+    this->configure_fsk_ook_();
+  } else {
+    this->configure_lora_();
+  }
+
+  // switch to rx or sleep
+  if (this->rx_start_) {
+    this->set_mode_rx();
+  } else {
+    this->set_mode_sleep();
+  }
+}
+
+void SX127x::configure_fsk_ook_() {
+  // set the channel bw
+  this->write_register_(REG_RX_BW, BW_FSK_OOK[this->bandwidth_]);
+
+  // set fdev
+  uint32_t fdev = std::min((this->deviation_ * 4096) / 250000, (uint32_t) 0x3FFF);
+  this->write_register_(REG_FDEV_MSB, (uint8_t) ((fdev >> 8) & 0xFF));
+  this->write_register_(REG_FDEV_LSB, (uint8_t) ((fdev >> 0) & 0xFF));
+
+  // set bitrate
+  uint64_t bitrate = (FXOSC + this->bitrate_ / 2) / this->bitrate_;  // round up
+  this->write_register_(REG_BITRATE_MSB, (uint8_t) ((bitrate >> 8) & 0xFF));
+  this->write_register_(REG_BITRATE_LSB, (uint8_t) ((bitrate >> 0) & 0xFF));
+
+  // configure rx and afc
+  uint8_t trigger = (this->preamble_detect_ > 0) ? TRIGGER_PREAMBLE : TRIGGER_RSSI;
+  this->write_register_(REG_AFC_FEI, AFC_AUTO_CLEAR_ON);
+  if (this->modulation_ == MOD_FSK) {
+    this->write_register_(REG_RX_CONFIG, AFC_AUTO_ON | AGC_AUTO_ON | trigger);
+  } else {
+    this->write_register_(REG_RX_CONFIG, AGC_AUTO_ON | trigger);
+  }
+
+  // configure packet mode
+  if (this->packet_mode_) {
+    uint8_t crc_mode = (this->crc_enable_) ? CRC_ON : CRC_OFF;
+    this->write_register_(REG_FIFO_THRESH, TX_START_FIFO_EMPTY);
+    if (this->payload_length_ > 0) {
+      this->write_register_(REG_PAYLOAD_LENGTH_LSB, this->payload_length_);
+      this->write_register_(REG_PACKET_CONFIG_1, crc_mode | FIXED_LENGTH);
+    } else {
+      this->write_register_(REG_PAYLOAD_LENGTH_LSB, this->get_max_packet_size() - 1);
+      this->write_register_(REG_PACKET_CONFIG_1, crc_mode | VARIABLE_LENGTH);
+    }
+    this->write_register_(REG_PACKET_CONFIG_2, PACKET_MODE);
+  } else {
+    this->write_register_(REG_PACKET_CONFIG_2, CONTINUOUS_MODE);
+  }
+  this->write_register_(REG_DIO_MAPPING1, DIO0_MAPPING_00);
+
+  // config bit synchronizer
+  uint8_t polarity = (this->preamble_polarity_ == 0xAA) ? PREAMBLE_AA : PREAMBLE_55;
+  if (!this->sync_value_.empty()) {
+    uint8_t size = this->sync_value_.size() - 1;
+    this->write_register_(REG_SYNC_CONFIG, AUTO_RESTART_PLL_LOCK | polarity | SYNC_ON | size);
+    for (uint32_t i = 0; i < this->sync_value_.size(); i++) {
+      this->write_register_(REG_SYNC_VALUE1 + i, this->sync_value_[i]);
+    }
+  } else {
+    this->write_register_(REG_SYNC_CONFIG, AUTO_RESTART_PLL_LOCK | polarity);
+  }
+
+  // config preamble detector
+  if (this->preamble_detect_ > 0) {
+    uint8_t size = (this->preamble_detect_ - 1) << PREAMBLE_DETECTOR_SIZE_SHIFT;
+    uint8_t tol = this->preamble_errors_ << PREAMBLE_DETECTOR_TOL_SHIFT;
+    this->write_register_(REG_PREAMBLE_DETECT, PREAMBLE_DETECTOR_ON | size | tol);
+  } else {
+    this->write_register_(REG_PREAMBLE_DETECT, PREAMBLE_DETECTOR_OFF);
+  }
+  this->write_register_(REG_PREAMBLE_SIZE_MSB, this->preamble_size_ >> 16);
+  this->write_register_(REG_PREAMBLE_SIZE_LSB, this->preamble_size_ & 0xFF);
+
+  // config sync generation and setup ook threshold
+  uint8_t bitsync = this->bitsync_ ? BIT_SYNC_ON : BIT_SYNC_OFF;
+  this->write_register_(REG_OOK_PEAK, bitsync | OOK_THRESH_STEP_0_5 | OOK_THRESH_PEAK);
+  this->write_register_(REG_OOK_AVG, OOK_AVG_RESERVED | OOK_THRESH_DEC_1_8);
+
+  // set rx floor
+  this->write_register_(REG_OOK_FIX, 256 + int(this->rx_floor_ * 2.0));
+  this->write_register_(REG_RSSI_THRESH, std::abs(int(this->rx_floor_ * 2.0)));
+}
+
+void SX127x::configure_lora_() {
+  // config modem
+  uint8_t header_mode = this->payload_length_ > 0 ? IMPLICIT_HEADER : EXPLICIT_HEADER;
+  uint8_t crc_mode = (this->crc_enable_) ? RX_PAYLOAD_CRC_ON : RX_PAYLOAD_CRC_OFF;
+  uint8_t spreading_factor = this->spreading_factor_ << SPREADING_FACTOR_SHIFT;
+  this->write_register_(REG_MODEM_CONFIG1, BW_LORA[this->bandwidth_] | this->coding_rate_ | header_mode);
+  this->write_register_(REG_MODEM_CONFIG2, spreading_factor | crc_mode);
+
+  // config fifo and payload length
+  this->write_register_(REG_FIFO_TX_BASE_ADDR, 0x00);
+  this->write_register_(REG_FIFO_RX_BASE_ADDR, 0x00);
+  this->write_register_(REG_PAYLOAD_LENGTH, std::max(this->payload_length_, (uint32_t) 1));
+
+  // config preamble
+  if (this->preamble_size_ >= 6) {
+    this->write_register_(REG_PREAMBLE_LEN_MSB, this->preamble_size_ >> 16);
+    this->write_register_(REG_PREAMBLE_LEN_LSB, this->preamble_size_ & 0xFF);
+  }
+
+  // optimize detection
+  float duration = 1000.0f * std::pow(2, this->spreading_factor_) / BW_HZ[this->bandwidth_];
+  if (duration > 16) {
+    this->write_register_(REG_MODEM_CONFIG3, MODEM_AGC_AUTO_ON | LOW_DATA_RATE_OPTIMIZE_ON);
+  } else {
+    this->write_register_(REG_MODEM_CONFIG3, MODEM_AGC_AUTO_ON);
+  }
+  if (this->spreading_factor_ == 6) {
+    this->write_register_(REG_DETECT_OPTIMIZE, 0xC5);
+    this->write_register_(REG_DETECT_THRESHOLD, 0x0C);
+  } else {
+    this->write_register_(REG_DETECT_OPTIMIZE, 0xC3);
+    this->write_register_(REG_DETECT_THRESHOLD, 0x0A);
+  }
+
+  // config sync word
+  if (!this->sync_value_.empty()) {
+    this->write_register_(REG_SYNC_WORD, this->sync_value_[0]);
+  }
+}
+
+size_t SX127x::get_max_packet_size() {
+  if (this->payload_length_ > 0) {
+    return this->payload_length_;
+  }
+  if (this->modulation_ == MOD_LORA) {
+    return 256;
+  } else {
+    return 64;
+  }
+}
+
+void SX127x::transmit_packet(const std::vector &packet) {
+  if (this->payload_length_ > 0 && this->payload_length_ != packet.size()) {
+    ESP_LOGE(TAG, "Packet size does not match config");
+    return;
+  }
+  if (packet.empty() || packet.size() > this->get_max_packet_size()) {
+    ESP_LOGE(TAG, "Packet size out of range");
+    return;
+  }
+  if (this->modulation_ == MOD_LORA) {
+    this->set_mode_standby();
+    if (this->payload_length_ == 0) {
+      this->write_register_(REG_PAYLOAD_LENGTH, packet.size());
+    }
+    this->write_register_(REG_IRQ_FLAGS, 0xFF);
+    this->write_register_(REG_FIFO_ADDR_PTR, 0);
+    this->write_fifo_(packet);
+    this->set_mode_tx();
+  } else {
+    this->set_mode_standby();
+    if (this->payload_length_ == 0) {
+      this->write_register_(REG_FIFO, packet.size());
+    }
+    this->write_fifo_(packet);
+    this->set_mode_tx();
+  }
+  // wait until transmit completes, typically the delay will be less than 100 ms
+  uint32_t start = millis();
+  while (!this->dio0_pin_->digital_read()) {
+    if (millis() - start > 4000) {
+      ESP_LOGE(TAG, "Transmit packet failure");
+      break;
+    }
+  }
+  if (this->rx_start_) {
+    this->set_mode_rx();
+  } else {
+    this->set_mode_sleep();
+  }
+}
+
+void SX127x::call_listeners_(const std::vector &packet, float rssi, float snr) {
+  for (auto &listener : this->listeners_) {
+    listener->on_packet(packet, rssi, snr);
+  }
+  this->packet_trigger_->trigger(packet, rssi, snr);
+}
+
+void SX127x::loop() {
+  if (this->dio0_pin_ == nullptr || !this->dio0_pin_->digital_read()) {
+    return;
+  }
+
+  if (this->modulation_ == MOD_LORA) {
+    uint8_t status = this->read_register_(REG_IRQ_FLAGS);
+    this->write_register_(REG_IRQ_FLAGS, 0xFF);
+    if ((status & PAYLOAD_CRC_ERROR) == 0) {
+      uint8_t bytes = this->read_register_(REG_NB_RX_BYTES);
+      uint8_t addr = this->read_register_(REG_FIFO_RX_CURR_ADDR);
+      uint8_t rssi = this->read_register_(REG_PKT_RSSI_VALUE);
+      int8_t snr = (int8_t) this->read_register_(REG_PKT_SNR_VALUE);
+      std::vector packet(bytes);
+      this->write_register_(REG_FIFO_ADDR_PTR, addr);
+      this->read_fifo_(packet);
+      if (this->frequency_ > 700000000) {
+        this->call_listeners_(packet, (float) rssi - RSSI_OFFSET_HF, (float) snr / 4);
+      } else {
+        this->call_listeners_(packet, (float) rssi - RSSI_OFFSET_LF, (float) snr / 4);
+      }
+    }
+  } else if (this->packet_mode_) {
+    std::vector packet;
+    uint8_t payload_length = this->payload_length_;
+    if (payload_length == 0) {
+      payload_length = this->read_register_(REG_FIFO);
+    }
+    packet.resize(payload_length);
+    this->read_fifo_(packet);
+    this->call_listeners_(packet, 0.0f, 0.0f);
+  }
+}
+
+void SX127x::run_image_cal() {
+  uint32_t start = millis();
+  uint8_t mode = this->read_register_(REG_OP_MODE);
+  if ((mode & MODE_MASK) != MODE_STDBY) {
+    ESP_LOGE(TAG, "Need to be in standby for image cal");
+    return;
+  }
+  if (mode & MOD_LORA) {
+    this->set_mode_(MOD_FSK, MODE_SLEEP);
+    this->set_mode_(MOD_FSK, MODE_STDBY);
+  }
+  if (this->auto_cal_) {
+    this->write_register_(REG_IMAGE_CAL, IMAGE_CAL_START | AUTO_IMAGE_CAL_ON | TEMP_THRESHOLD_10C);
+  } else {
+    this->write_register_(REG_IMAGE_CAL, IMAGE_CAL_START);
+  }
+  while (this->read_register_(REG_IMAGE_CAL) & IMAGE_CAL_RUNNING) {
+    if (millis() - start > 20) {
+      ESP_LOGE(TAG, "Image cal failure");
+      break;
+    }
+  }
+  if (mode & MOD_LORA) {
+    this->set_mode_(this->modulation_, MODE_SLEEP);
+    this->set_mode_(this->modulation_, MODE_STDBY);
+  }
+}
+
+void SX127x::set_mode_(uint8_t modulation, uint8_t mode) {
+  uint32_t start = millis();
+  this->write_register_(REG_OP_MODE, modulation | mode);
+  while (true) {
+    uint8_t curr = this->read_register_(REG_OP_MODE) & MODE_MASK;
+    if ((curr == mode) || (mode == MODE_RX && curr == MODE_RX_FS)) {
+      if (mode == MODE_SLEEP) {
+        this->write_register_(REG_OP_MODE, modulation | mode);
+      }
+      break;
+    }
+    if (millis() - start > 20) {
+      ESP_LOGE(TAG, "Set mode failure");
+      break;
+    }
+  }
+}
+
+void SX127x::set_mode_rx() {
+  this->set_mode_(this->modulation_, MODE_RX);
+  if (this->modulation_ == MOD_LORA) {
+    this->write_register_(REG_IRQ_FLAGS_MASK, 0x00);
+    this->write_register_(REG_DIO_MAPPING1, DIO0_MAPPING_00);
+  }
+}
+
+void SX127x::set_mode_tx() {
+  this->set_mode_(this->modulation_, MODE_TX);
+  if (this->modulation_ == MOD_LORA) {
+    this->write_register_(REG_IRQ_FLAGS_MASK, 0x00);
+    this->write_register_(REG_DIO_MAPPING1, DIO0_MAPPING_01);
+  }
+}
+
+void SX127x::set_mode_standby() { this->set_mode_(this->modulation_, MODE_STDBY); }
+
+void SX127x::set_mode_sleep() { this->set_mode_(this->modulation_, MODE_SLEEP); }
+
+void SX127x::dump_config() {
+  ESP_LOGCONFIG(TAG, "SX127x:");
+  LOG_PIN("  CS Pin: ", this->cs_);
+  LOG_PIN("  RST Pin: ", this->rst_pin_);
+  LOG_PIN("  DIO0 Pin: ", this->dio0_pin_);
+  const char *shaping = "NONE";
+  if (this->shaping_ == CUTOFF_BR_X_2) {
+    shaping = "CUTOFF_BR_X_2";
+  } else if (this->shaping_ == CUTOFF_BR_X_1) {
+    shaping = "CUTOFF_BR_X_1";
+  } else if (this->shaping_ == GAUSSIAN_BT_0_3) {
+    shaping = "GAUSSIAN_BT_0_3";
+  } else if (this->shaping_ == GAUSSIAN_BT_0_5) {
+    shaping = "GAUSSIAN_BT_0_5";
+  } else if (this->shaping_ == GAUSSIAN_BT_1_0) {
+    shaping = "GAUSSIAN_BT_1_0";
+  }
+  const char *pa_pin = "RFO";
+  if (this->pa_pin_ == PA_PIN_BOOST) {
+    pa_pin = "BOOST";
+  }
+  ESP_LOGCONFIG(TAG,
+                "  Auto Cal: %s\n"
+                "  Frequency: %" PRIu32 " Hz\n"
+                "  Bandwidth: %" PRIu32 " Hz\n"
+                "  PA Pin: %s\n"
+                "  PA Power: %" PRIu8 " dBm\n"
+                "  PA Ramp: %" PRIu16 " us\n"
+                "  Shaping: %s",
+                TRUEFALSE(this->auto_cal_), this->frequency_, BW_HZ[this->bandwidth_], pa_pin, this->pa_power_,
+                RAMP[this->pa_ramp_], shaping);
+  if (this->modulation_ == MOD_FSK) {
+    ESP_LOGCONFIG(TAG, "  Deviation: %" PRIu32 " Hz", this->deviation_);
+  }
+  if (this->modulation_ == MOD_LORA) {
+    const char *cr = "4/8";
+    if (this->coding_rate_ == CODING_RATE_4_5) {
+      cr = "4/5";
+    } else if (this->coding_rate_ == CODING_RATE_4_6) {
+      cr = "4/6";
+    } else if (this->coding_rate_ == CODING_RATE_4_7) {
+      cr = "4/7";
+    }
+    ESP_LOGCONFIG(TAG,
+                  "  Modulation: LORA\n"
+                  "  Preamble Size: %" PRIu16 "\n"
+                  "  Spreading Factor: %" PRIu8 "\n"
+                  "  Coding Rate: %s\n"
+                  "  CRC Enable: %s",
+                  this->preamble_size_, this->spreading_factor_, cr, TRUEFALSE(this->crc_enable_));
+    if (this->payload_length_ > 0) {
+      ESP_LOGCONFIG(TAG, "  Payload Length: %" PRIu32, this->payload_length_);
+    }
+    if (!this->sync_value_.empty()) {
+      ESP_LOGCONFIG(TAG, "  Sync Value: 0x%02x", this->sync_value_[0]);
+    }
+  } else {
+    ESP_LOGCONFIG(TAG,
+                  "  Modulation: %s\n"
+                  "  Bitrate: %" PRIu32 "b/s\n"
+                  "  Bitsync: %s\n"
+                  "  Rx Start: %s\n"
+                  "  Rx Floor: %.1f dBm\n"
+                  "  Packet Mode: %s",
+                  this->modulation_ == MOD_FSK ? "FSK" : "OOK", this->bitrate_, TRUEFALSE(this->bitsync_),
+                  TRUEFALSE(this->rx_start_), this->rx_floor_, TRUEFALSE(this->packet_mode_));
+    if (this->packet_mode_) {
+      ESP_LOGCONFIG(TAG, "  CRC Enable: %s", TRUEFALSE(this->crc_enable_));
+    }
+    if (this->payload_length_ > 0) {
+      ESP_LOGCONFIG(TAG, "  Payload Length: %" PRIu32, this->payload_length_);
+    }
+    if (!this->sync_value_.empty()) {
+      ESP_LOGCONFIG(TAG, "  Sync Value: 0x%s", format_hex(this->sync_value_).c_str());
+    }
+    if (this->preamble_size_ > 0 || this->preamble_detect_ > 0) {
+      ESP_LOGCONFIG(TAG,
+                    "  Preamble Polarity: 0x%X\n"
+                    "  Preamble Size: %" PRIu16 "\n"
+                    "  Preamble Detect: %" PRIu8 "\n"
+                    "  Preamble Errors: %" PRIu8,
+                    this->preamble_polarity_, this->preamble_size_, this->preamble_detect_, this->preamble_errors_);
+    }
+  }
+  if (this->is_failed()) {
+    ESP_LOGE(TAG, "Configuring SX127x failed");
+  }
+}
+
+}  // namespace sx127x
+}  // namespace esphome
diff --git a/esphome/components/sx127x/sx127x.h b/esphome/components/sx127x/sx127x.h
new file mode 100644
index 0000000000..fe9f60e860
--- /dev/null
+++ b/esphome/components/sx127x/sx127x.h
@@ -0,0 +1,125 @@
+#pragma once
+
+#include "sx127x_reg.h"
+#include "esphome/components/spi/spi.h"
+#include "esphome/core/automation.h"
+#include "esphome/core/component.h"
+#include 
+
+namespace esphome {
+namespace sx127x {
+
+enum SX127xBw : uint8_t {
+  SX127X_BW_2_6,
+  SX127X_BW_3_1,
+  SX127X_BW_3_9,
+  SX127X_BW_5_2,
+  SX127X_BW_6_3,
+  SX127X_BW_7_8,
+  SX127X_BW_10_4,
+  SX127X_BW_12_5,
+  SX127X_BW_15_6,
+  SX127X_BW_20_8,
+  SX127X_BW_25_0,
+  SX127X_BW_31_3,
+  SX127X_BW_41_7,
+  SX127X_BW_50_0,
+  SX127X_BW_62_5,
+  SX127X_BW_83_3,
+  SX127X_BW_100_0,
+  SX127X_BW_125_0,
+  SX127X_BW_166_7,
+  SX127X_BW_200_0,
+  SX127X_BW_250_0,
+  SX127X_BW_500_0,
+};
+
+class SX127xListener {
+ public:
+  virtual void on_packet(const std::vector &packet, float rssi, float snr) = 0;
+};
+
+class SX127x : public Component,
+               public spi::SPIDevice {
+ public:
+  size_t get_max_packet_size();
+  float get_setup_priority() const override { return setup_priority::PROCESSOR; }
+  void setup() override;
+  void loop() override;
+  void dump_config() override;
+  void set_auto_cal(bool auto_cal) { this->auto_cal_ = auto_cal; }
+  void set_bandwidth(SX127xBw bandwidth) { this->bandwidth_ = bandwidth; }
+  void set_bitrate(uint32_t bitrate) { this->bitrate_ = bitrate; }
+  void set_bitsync(bool bitsync) { this->bitsync_ = bitsync; }
+  void set_coding_rate(uint8_t coding_rate) { this->coding_rate_ = coding_rate; }
+  void set_crc_enable(bool crc_enable) { this->crc_enable_ = crc_enable; }
+  void set_deviation(uint32_t deviation) { this->deviation_ = deviation; }
+  void set_dio0_pin(InternalGPIOPin *dio0_pin) { this->dio0_pin_ = dio0_pin; }
+  void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
+  void set_mode_rx();
+  void set_mode_tx();
+  void set_mode_standby();
+  void set_mode_sleep();
+  void set_modulation(uint8_t modulation) { this->modulation_ = modulation; }
+  void set_pa_pin(uint8_t pin) { this->pa_pin_ = pin; }
+  void set_pa_power(uint8_t power) { this->pa_power_ = power; }
+  void set_pa_ramp(uint8_t ramp) { this->pa_ramp_ = ramp; }
+  void set_packet_mode(bool packet_mode) { this->packet_mode_ = packet_mode; }
+  void set_payload_length(uint8_t payload_length) { this->payload_length_ = payload_length; }
+  void set_preamble_errors(uint8_t preamble_errors) { this->preamble_errors_ = preamble_errors; }
+  void set_preamble_polarity(uint8_t preamble_polarity) { this->preamble_polarity_ = preamble_polarity; }
+  void set_preamble_size(uint16_t preamble_size) { this->preamble_size_ = preamble_size; }
+  void set_preamble_detect(uint8_t preamble_detect) { this->preamble_detect_ = preamble_detect; }
+  void set_rst_pin(InternalGPIOPin *rst_pin) { this->rst_pin_ = rst_pin; }
+  void set_rx_floor(float floor) { this->rx_floor_ = floor; }
+  void set_rx_start(bool start) { this->rx_start_ = start; }
+  void set_shaping(uint8_t shaping) { this->shaping_ = shaping; }
+  void set_spreading_factor(uint8_t spreading_factor) { this->spreading_factor_ = spreading_factor; }
+  void set_sync_value(const std::vector &sync_value) { this->sync_value_ = sync_value; }
+  void run_image_cal();
+  void configure();
+  void transmit_packet(const std::vector &packet);
+  void register_listener(SX127xListener *listener) { this->listeners_.push_back(listener); }
+  Trigger, float, float> *get_packet_trigger() const { return this->packet_trigger_; };
+
+ protected:
+  void configure_fsk_ook_();
+  void configure_lora_();
+  void set_mode_(uint8_t modulation, uint8_t mode);
+  void write_fifo_(const std::vector &packet);
+  void read_fifo_(std::vector &packet);
+  void write_register_(uint8_t reg, uint8_t value);
+  void call_listeners_(const std::vector &packet, float rssi, float snr);
+  uint8_t read_register_(uint8_t reg);
+  Trigger, float, float> *packet_trigger_{new Trigger, float, float>()};
+  std::vector listeners_;
+  std::vector sync_value_;
+  InternalGPIOPin *dio0_pin_{nullptr};
+  InternalGPIOPin *rst_pin_{nullptr};
+  SX127xBw bandwidth_;
+  uint32_t bitrate_;
+  uint32_t deviation_;
+  uint32_t frequency_;
+  uint32_t payload_length_;
+  uint16_t preamble_size_;
+  uint8_t coding_rate_;
+  uint8_t modulation_;
+  uint8_t pa_pin_;
+  uint8_t pa_power_;
+  uint8_t pa_ramp_;
+  uint8_t preamble_detect_;
+  uint8_t preamble_errors_;
+  uint8_t preamble_polarity_;
+  uint8_t shaping_;
+  uint8_t spreading_factor_;
+  float rx_floor_;
+  bool auto_cal_{false};
+  bool bitsync_{false};
+  bool crc_enable_{false};
+  bool packet_mode_{false};
+  bool rx_start_{false};
+};
+
+}  // namespace sx127x
+}  // namespace esphome
diff --git a/esphome/components/sx127x/sx127x_reg.h b/esphome/components/sx127x/sx127x_reg.h
new file mode 100644
index 0000000000..d5e9c50957
--- /dev/null
+++ b/esphome/components/sx127x/sx127x_reg.h
@@ -0,0 +1,295 @@
+#pragma once
+
+#include "esphome/core/hal.h"
+
+namespace esphome {
+namespace sx127x {
+
+enum SX127xReg : uint8_t {
+  // Common registers
+  REG_FIFO = 0x00,
+  REG_OP_MODE = 0x01,
+  REG_BITRATE_MSB = 0x02,
+  REG_BITRATE_LSB = 0x03,
+  REG_FDEV_MSB = 0x04,
+  REG_FDEV_LSB = 0x05,
+  REG_FRF_MSB = 0x06,
+  REG_FRF_MID = 0x07,
+  REG_FRF_LSB = 0x08,
+  REG_PA_CONFIG = 0x09,
+  REG_PA_RAMP = 0x0A,
+  REG_DIO_MAPPING1 = 0x40,
+  REG_DIO_MAPPING2 = 0x41,
+  REG_VERSION = 0x42,
+  // FSK/OOK registers
+  REG_RX_CONFIG = 0x0D,
+  REG_RSSI_THRESH = 0x10,
+  REG_RX_BW = 0x12,
+  REG_OOK_PEAK = 0x14,
+  REG_OOK_FIX = 0x15,
+  REG_OOK_AVG = 0x16,
+  REG_AFC_FEI = 0x1A,
+  REG_PREAMBLE_DETECT = 0x1F,
+  REG_PREAMBLE_SIZE_MSB = 0x25,
+  REG_PREAMBLE_SIZE_LSB = 0x26,
+  REG_SYNC_CONFIG = 0x27,
+  REG_SYNC_VALUE1 = 0x28,
+  REG_SYNC_VALUE2 = 0x29,
+  REG_SYNC_VALUE3 = 0x2A,
+  REG_SYNC_VALUE4 = 0x2B,
+  REG_SYNC_VALUE5 = 0x2C,
+  REG_SYNC_VALUE6 = 0x2D,
+  REG_SYNC_VALUE7 = 0x2E,
+  REG_SYNC_VALUE8 = 0x2F,
+  REG_PACKET_CONFIG_1 = 0x30,
+  REG_PACKET_CONFIG_2 = 0x31,
+  REG_PAYLOAD_LENGTH_LSB = 0x32,
+  REG_FIFO_THRESH = 0x35,
+  REG_IMAGE_CAL = 0x3B,
+  // LoRa registers
+  REG_FIFO_ADDR_PTR = 0x0D,
+  REG_FIFO_TX_BASE_ADDR = 0x0E,
+  REG_FIFO_RX_BASE_ADDR = 0x0F,
+  REG_FIFO_RX_CURR_ADDR = 0x10,
+  REG_IRQ_FLAGS_MASK = 0x11,
+  REG_IRQ_FLAGS = 0x12,
+  REG_NB_RX_BYTES = 0x13,
+  REG_MODEM_STAT = 0x18,
+  REG_PKT_SNR_VALUE = 0x19,
+  REG_PKT_RSSI_VALUE = 0x1A,
+  REG_RSSI_VALUE = 0x1B,
+  REG_HOP_CHANNEL = 0x1C,
+  REG_MODEM_CONFIG1 = 0x1D,
+  REG_MODEM_CONFIG2 = 0x1E,
+  REG_SYMB_TIMEOUT_LSB = 0x1F,
+  REG_PREAMBLE_LEN_MSB = 0x20,
+  REG_PREAMBLE_LEN_LSB = 0x21,
+  REG_PAYLOAD_LENGTH = 0x22,
+  REG_HOP_PERIOD = 0x24,
+  REG_FIFO_RX_BYTE_ADDR = 0x25,
+  REG_MODEM_CONFIG3 = 0x26,
+  REG_FEI_MSB = 0x28,
+  REG_FEI_MIB = 0x29,
+  REG_FEI_LSB = 0x2A,
+  REG_DETECT_OPTIMIZE = 0x31,
+  REG_INVERT_IQ = 0x33,
+  REG_DETECT_THRESHOLD = 0x37,
+  REG_SYNC_WORD = 0x39,
+};
+
+enum SX127xOpMode : uint8_t {
+  MOD_LORA = 0x80,
+  ACCESS_FSK_REGS = 0x40,
+  ACCESS_LORA_REGS = 0x00,
+  MOD_OOK = 0x20,
+  MOD_FSK = 0x00,
+  ACCESS_LF_REGS = 0x08,
+  ACCESS_HF_REGS = 0x00,
+  MODE_CAD = 0x07,
+  MODE_RX_SINGLE = 0x06,
+  MODE_RX = 0x05,
+  MODE_RX_FS = 0x04,
+  MODE_TX = 0x03,
+  MODE_TX_FS = 0x02,
+  MODE_STDBY = 0x01,
+  MODE_SLEEP = 0x00,
+  MODE_MASK = 0x07,
+};
+
+enum SX127xPaConfig : uint8_t {
+  PA_PIN_BOOST = 0x80,
+  PA_PIN_RFO = 0x00,
+  PA_MAX_POWER = 0x70,
+};
+
+enum SX127xPaRamp : uint8_t {
+  CUTOFF_BR_X_2 = 0x40,
+  CUTOFF_BR_X_1 = 0x20,
+  GAUSSIAN_BT_0_3 = 0x60,
+  GAUSSIAN_BT_0_5 = 0x40,
+  GAUSSIAN_BT_1_0 = 0x20,
+  SHAPING_NONE = 0x00,
+  PA_RAMP_10 = 0x0F,
+  PA_RAMP_12 = 0x0E,
+  PA_RAMP_15 = 0x0D,
+  PA_RAMP_20 = 0x0C,
+  PA_RAMP_25 = 0x0B,
+  PA_RAMP_31 = 0x0A,
+  PA_RAMP_40 = 0x09,
+  PA_RAMP_50 = 0x08,
+  PA_RAMP_62 = 0x07,
+  PA_RAMP_100 = 0x06,
+  PA_RAMP_125 = 0x05,
+  PA_RAMP_250 = 0x04,
+  PA_RAMP_500 = 0x03,
+  PA_RAMP_1000 = 0x02,
+  PA_RAMP_2000 = 0x01,
+  PA_RAMP_3400 = 0x00,
+};
+
+enum SX127xDioMapping1 : uint8_t {
+  DIO0_MAPPING_00 = 0x00,
+  DIO0_MAPPING_01 = 0x40,
+  DIO0_MAPPING_10 = 0x80,
+  DIO0_MAPPING_11 = 0xC0,
+};
+
+enum SX127xRxConfig : uint8_t {
+  RESTART_ON_COLLISION = 0x80,
+  RESTART_NO_LOCK = 0x40,
+  RESTART_PLL_LOCK = 0x20,
+  AFC_AUTO_ON = 0x10,
+  AGC_AUTO_ON = 0x08,
+  TRIGGER_NONE = 0x00,
+  TRIGGER_RSSI = 0x01,
+  TRIGGER_PREAMBLE = 0x06,
+  TRIGGER_ALL = 0x07,
+};
+
+enum SX127xRxBw : uint8_t {
+  RX_BW_2_6 = 0x17,
+  RX_BW_3_1 = 0x0F,
+  RX_BW_3_9 = 0x07,
+  RX_BW_5_2 = 0x16,
+  RX_BW_6_3 = 0x0E,
+  RX_BW_7_8 = 0x06,
+  RX_BW_10_4 = 0x15,
+  RX_BW_12_5 = 0x0D,
+  RX_BW_15_6 = 0x05,
+  RX_BW_20_8 = 0x14,
+  RX_BW_25_0 = 0x0C,
+  RX_BW_31_3 = 0x04,
+  RX_BW_41_7 = 0x13,
+  RX_BW_50_0 = 0x0B,
+  RX_BW_62_5 = 0x03,
+  RX_BW_83_3 = 0x12,
+  RX_BW_100_0 = 0x0A,
+  RX_BW_125_0 = 0x02,
+  RX_BW_166_7 = 0x11,
+  RX_BW_200_0 = 0x09,
+  RX_BW_250_0 = 0x01,
+};
+
+enum SX127xOokPeak : uint8_t {
+  BIT_SYNC_ON = 0x20,
+  BIT_SYNC_OFF = 0x00,
+  OOK_THRESH_AVG = 0x10,
+  OOK_THRESH_PEAK = 0x08,
+  OOK_THRESH_FIXED = 0x00,
+  OOK_THRESH_STEP_6_0 = 0x07,
+  OOK_THRESH_STEP_5_0 = 0x06,
+  OOK_THRESH_STEP_4_0 = 0x05,
+  OOK_THRESH_STEP_3_0 = 0x04,
+  OOK_THRESH_STEP_2_0 = 0x03,
+  OOK_THRESH_STEP_1_5 = 0x02,
+  OOK_THRESH_STEP_1_0 = 0x01,
+  OOK_THRESH_STEP_0_5 = 0x00,
+};
+
+enum SX127xOokAvg : uint8_t {
+  OOK_THRESH_DEC_16 = 0xE0,
+  OOK_THRESH_DEC_8 = 0xC0,
+  OOK_THRESH_DEC_4 = 0xA0,
+  OOK_THRESH_DEC_2 = 0x80,
+  OOK_THRESH_DEC_1_8 = 0x60,
+  OOK_THRESH_DEC_1_4 = 0x40,
+  OOK_THRESH_DEC_1_2 = 0x20,
+  OOK_THRESH_DEC_1 = 0x00,
+  OOK_AVG_RESERVED = 0x10,
+};
+
+enum SX127xAfcFei : uint8_t {
+  AFC_AUTO_CLEAR_ON = 0x01,
+};
+
+enum SX127xPreambleDetect : uint8_t {
+  PREAMBLE_DETECTOR_ON = 0x80,
+  PREAMBLE_DETECTOR_OFF = 0x00,
+  PREAMBLE_DETECTOR_SIZE_SHIFT = 5,
+  PREAMBLE_DETECTOR_TOL_SHIFT = 0,
+};
+
+enum SX127xSyncConfig : uint8_t {
+  AUTO_RESTART_PLL_LOCK = 0x80,
+  AUTO_RESTART_NO_LOCK = 0x40,
+  AUTO_RESTART_OFF = 0x00,
+  PREAMBLE_55 = 0x20,
+  PREAMBLE_AA = 0x00,
+  SYNC_ON = 0x10,
+  SYNC_OFF = 0x00,
+};
+
+enum SX127xPacketConfig1 : uint8_t {
+  VARIABLE_LENGTH = 0x80,
+  FIXED_LENGTH = 0x00,
+  CRC_ON = 0x10,
+  CRC_OFF = 0x00,
+};
+
+enum SX127xPacketConfig2 : uint8_t {
+  CONTINUOUS_MODE = 0x00,
+  PACKET_MODE = 0x40,
+};
+
+enum SX127xFifoThresh : uint8_t {
+  TX_START_FIFO_EMPTY = 0x80,
+  TX_START_FIFO_LEVEL = 0x00,
+};
+
+enum SX127xImageCal : uint8_t {
+  AUTO_IMAGE_CAL_ON = 0x80,
+  IMAGE_CAL_START = 0x40,
+  IMAGE_CAL_RUNNING = 0x20,
+  TEMP_CHANGE = 0x08,
+  TEMP_THRESHOLD_20C = 0x06,
+  TEMP_THRESHOLD_15C = 0x04,
+  TEMP_THRESHOLD_10C = 0x02,
+  TEMP_THRESHOLD_5C = 0x00,
+  TEMP_MONITOR_OFF = 0x01,
+  TEMP_MONITOR_ON = 0x00,
+};
+
+enum SX127xIrqFlags : uint8_t {
+  RX_TIMEOUT = 0x80,
+  RX_DONE = 0x40,
+  PAYLOAD_CRC_ERROR = 0x20,
+  VALID_HEADER = 0x10,
+  TX_DONE = 0x08,
+  CAD_DONE = 0x04,
+  FHSS_CHANGE_CHANNEL = 0x02,
+  CAD_DETECTED = 0x01,
+};
+
+enum SX127xModemCfg1 : uint8_t {
+  BW_7_8 = 0x00,
+  BW_10_4 = 0x10,
+  BW_15_6 = 0x20,
+  BW_20_8 = 0x30,
+  BW_31_3 = 0x40,
+  BW_41_7 = 0x50,
+  BW_62_5 = 0x60,
+  BW_125_0 = 0x70,
+  BW_250_0 = 0x80,
+  BW_500_0 = 0x90,
+  CODING_RATE_4_5 = 0x02,
+  CODING_RATE_4_6 = 0x04,
+  CODING_RATE_4_7 = 0x06,
+  CODING_RATE_4_8 = 0x08,
+  IMPLICIT_HEADER = 0x01,
+  EXPLICIT_HEADER = 0x00,
+};
+
+enum SX127xModemCfg2 : uint8_t {
+  SPREADING_FACTOR_SHIFT = 4,
+  TX_CONTINOUS_MODE = 0x08,
+  RX_PAYLOAD_CRC_ON = 0x04,
+  RX_PAYLOAD_CRC_OFF = 0x00,
+};
+
+enum SX127xModemCfg3 : uint8_t {
+  LOW_DATA_RATE_OPTIMIZE_ON = 0x08,
+  MODEM_AGC_AUTO_ON = 0x04,
+};
+
+}  // namespace sx127x
+}  // namespace esphome
diff --git a/tests/components/sx127x/common.yaml b/tests/components/sx127x/common.yaml
new file mode 100644
index 0000000000..63adc2e91c
--- /dev/null
+++ b/tests/components/sx127x/common.yaml
@@ -0,0 +1,45 @@
+spi:
+  clk_pin: ${clk_pin}
+  mosi_pin: ${mosi_pin}
+  miso_pin: ${miso_pin}
+
+sx127x:
+  cs_pin: ${cs_pin}
+  rst_pin: ${rst_pin}
+  dio0_pin: ${dio0_pin}
+  pa_pin: BOOST
+  pa_power: 17
+  pa_ramp: 40us
+  bitsync: true
+  bitrate: 4800
+  bandwidth: 50_0kHz
+  frequency: 433920000
+  modulation: FSK
+  deviation: 5000
+  rx_start: true
+  rx_floor: -90
+  packet_mode: true
+  payload_length: 8
+  sync_value: [0x33, 0x33]
+  shaping: NONE
+  preamble_size: 2
+  preamble_detect: 2
+  preamble_errors: 8
+  preamble_polarity: 0x55
+  on_packet:
+    then:
+      - sx127x.send_packet:
+          data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C]
+
+button:
+  - platform: template
+    name: "SX127x Button"
+    on_press:
+      then:
+        - sx127x.set_mode_standby
+        - sx127x.run_image_cal
+        - sx127x.set_mode_tx
+        - sx127x.set_mode_sleep
+        - sx127x.set_mode_rx
+        - sx127x.send_packet:
+            data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C]
diff --git a/tests/components/sx127x/test.esp32-ard.yaml b/tests/components/sx127x/test.esp32-ard.yaml
new file mode 100644
index 0000000000..71270462a2
--- /dev/null
+++ b/tests/components/sx127x/test.esp32-ard.yaml
@@ -0,0 +1,9 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO27
+  miso_pin: GPIO19
+  cs_pin: GPIO18
+  rst_pin: GPIO23
+  dio0_pin: GPIO26
+
+<<: !include common.yaml
diff --git a/tests/components/sx127x/test.esp32-c3-ard.yaml b/tests/components/sx127x/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..36535a950d
--- /dev/null
+++ b/tests/components/sx127x/test.esp32-c3-ard.yaml
@@ -0,0 +1,9 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO18
+  miso_pin: GPIO19
+  cs_pin: GPIO1
+  rst_pin: GPIO2
+  dio0_pin: GPIO3
+
+<<: !include common.yaml
diff --git a/tests/components/sx127x/test.esp32-c3-idf.yaml b/tests/components/sx127x/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..36535a950d
--- /dev/null
+++ b/tests/components/sx127x/test.esp32-c3-idf.yaml
@@ -0,0 +1,9 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO18
+  miso_pin: GPIO19
+  cs_pin: GPIO1
+  rst_pin: GPIO2
+  dio0_pin: GPIO3
+
+<<: !include common.yaml
diff --git a/tests/components/sx127x/test.esp32-idf.yaml b/tests/components/sx127x/test.esp32-idf.yaml
new file mode 100644
index 0000000000..71270462a2
--- /dev/null
+++ b/tests/components/sx127x/test.esp32-idf.yaml
@@ -0,0 +1,9 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO27
+  miso_pin: GPIO19
+  cs_pin: GPIO18
+  rst_pin: GPIO23
+  dio0_pin: GPIO26
+
+<<: !include common.yaml
diff --git a/tests/components/sx127x/test.esp8266-ard.yaml b/tests/components/sx127x/test.esp8266-ard.yaml
new file mode 100644
index 0000000000..64c01edd44
--- /dev/null
+++ b/tests/components/sx127x/test.esp8266-ard.yaml
@@ -0,0 +1,9 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO13
+  miso_pin: GPIO12
+  cs_pin: GPIO1
+  rst_pin: GPIO2
+  dio0_pin: GPIO3
+
+<<: !include common.yaml
diff --git a/tests/components/sx127x/test.rp2040-ard.yaml b/tests/components/sx127x/test.rp2040-ard.yaml
new file mode 100644
index 0000000000..0af7b29790
--- /dev/null
+++ b/tests/components/sx127x/test.rp2040-ard.yaml
@@ -0,0 +1,9 @@
+substitutions:
+  clk_pin: GPIO2
+  mosi_pin: GPIO3
+  miso_pin: GPIO4
+  cs_pin: GPIO5
+  rst_pin: GPIO6
+  dio0_pin: GPIO7
+
+<<: !include common.yaml

From 547c7d6dc82b0b01e3c1cf1395545f32c0785cdb Mon Sep 17 00:00:00 2001
From: Kevin Ahrendt 
Date: Thu, 3 Jul 2025 17:17:01 +0100
Subject: [PATCH 224/293] [microphone] simplify mute handling to avoid
 unnecessary copies (#9303)

---
 esphome/components/microphone/microphone.cpp | 16 +++++++---------
 esphome/components/microphone/microphone.h   |  2 --
 2 files changed, 7 insertions(+), 11 deletions(-)

diff --git a/esphome/components/microphone/microphone.cpp b/esphome/components/microphone/microphone.cpp
index b1289f3791..0fbb393fd2 100644
--- a/esphome/components/microphone/microphone.cpp
+++ b/esphome/components/microphone/microphone.cpp
@@ -5,17 +5,15 @@ namespace microphone {
 
 void Microphone::add_data_callback(std::function &)> &&data_callback) {
   std::function &)> mute_handled_callback =
-      [this, data_callback](const std::vector &data) { data_callback(this->silence_audio_(data)); };
+      [this, data_callback](const std::vector &data) {
+        if (this->mute_state_) {
+          data_callback(std::vector(data.size(), 0));
+        } else {
+          data_callback(data);
+        };
+      };
   this->data_callbacks_.add(std::move(mute_handled_callback));
 }
 
-std::vector Microphone::silence_audio_(std::vector data) {
-  if (this->mute_state_) {
-    std::memset((void *) data.data(), 0, data.size());
-  }
-
-  return data;
-}
-
 }  // namespace microphone
 }  // namespace esphome
diff --git a/esphome/components/microphone/microphone.h b/esphome/components/microphone/microphone.h
index ea4e979e20..fcf9822458 100644
--- a/esphome/components/microphone/microphone.h
+++ b/esphome/components/microphone/microphone.h
@@ -33,8 +33,6 @@ class Microphone {
   audio::AudioStreamInfo get_audio_stream_info() { return this->audio_stream_info_; }
 
  protected:
-  std::vector silence_audio_(std::vector data);
-
   State state_{STATE_STOPPED};
   bool mute_state_{false};
 

From eef71a79da1a99ec8b57e8d2eea1209425c100dd Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 3 Jul 2025 21:49:51 +0000
Subject: [PATCH 225/293] Bump ruff from 0.12.1 to 0.12.2 (#9311)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston 
---
 .pre-commit-config.yaml | 2 +-
 requirements_test.txt   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 1a0289a3ca..831473c325 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,7 +4,7 @@
 repos:
   - repo: https://github.com/astral-sh/ruff-pre-commit
     # Ruff version.
-    rev: v0.12.1
+    rev: v0.12.2
     hooks:
       # Run the linter.
       - id: ruff
diff --git a/requirements_test.txt b/requirements_test.txt
index 66b71c2225..ef1fc4f2d6 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -1,6 +1,6 @@
 pylint==3.3.7
 flake8==7.3.0  # also change in .pre-commit-config.yaml when updating
-ruff==0.12.1  # also change in .pre-commit-config.yaml when updating
+ruff==0.12.2  # also change in .pre-commit-config.yaml when updating
 pyupgrade==3.20.0  # also change in .pre-commit-config.yaml when updating
 pre-commit
 

From 14d7c4bdbde327b74919af6f4c98fd181043bc27 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Thu, 3 Jul 2025 19:31:03 -0500
Subject: [PATCH 226/293] Add device_id to entity state messages for sub-device
 support (#9304)

---
 esphome/components/api/api.proto              |  21 +++
 esphome/components/api/api_connection.h       |   3 +
 esphome/components/api/api_pb2.cpp            | 132 ++++++++++++++
 esphome/components/api/api_pb2.h              |  44 ++---
 esphome/components/api/api_pb2_dump.cpp       | 105 ++++++++++++
 .../fixtures/device_id_in_state.yaml          |  85 +++++++++
 tests/integration/test_device_id_in_state.py  | 161 ++++++++++++++++++
 7 files changed, 530 insertions(+), 21 deletions(-)
 create mode 100644 tests/integration/fixtures/device_id_in_state.yaml
 create mode 100644 tests/integration/test_device_id_in_state.py

diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto
index 58a0b52555..a9aa0b4bff 100644
--- a/esphome/components/api/api.proto
+++ b/esphome/components/api/api.proto
@@ -311,6 +311,7 @@ message BinarySensorStateResponse {
   // If the binary sensor does not have a valid state yet.
   // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
   bool missing_state = 3;
+  uint32 device_id = 4;
 }
 
 // ==================== COVER ====================
@@ -360,6 +361,7 @@ message CoverStateResponse {
   float position = 3;
   float tilt = 4;
   CoverOperation current_operation = 5;
+  uint32 device_id = 6;
 }
 
 enum LegacyCoverCommand {
@@ -432,6 +434,7 @@ message FanStateResponse {
   FanDirection direction = 5;
   int32 speed_level = 6;
   string preset_mode = 7;
+  uint32 device_id = 8;
 }
 message FanCommandRequest {
   option (id) = 31;
@@ -513,6 +516,7 @@ message LightStateResponse {
   float cold_white = 12;
   float warm_white = 13;
   string effect = 9;
+  uint32 device_id = 14;
 }
 message LightCommandRequest {
   option (id) = 32;
@@ -598,6 +602,7 @@ message SensorStateResponse {
   // If the sensor does not have a valid state yet.
   // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
   bool missing_state = 3;
+  uint32 device_id = 4;
 }
 
 // ==================== SWITCH ====================
@@ -628,6 +633,7 @@ message SwitchStateResponse {
 
   fixed32 key = 1;
   bool state = 2;
+  uint32 device_id = 3;
 }
 message SwitchCommandRequest {
   option (id) = 33;
@@ -669,6 +675,7 @@ message TextSensorStateResponse {
   // If the text sensor does not have a valid state yet.
   // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
   bool missing_state = 3;
+  uint32 device_id = 4;
 }
 
 // ==================== SUBSCRIBE LOGS ====================
@@ -966,6 +973,7 @@ message ClimateStateResponse {
   string custom_preset = 13;
   float current_humidity = 14;
   float target_humidity = 15;
+  uint32 device_id = 16;
 }
 message ClimateCommandRequest {
   option (id) = 48;
@@ -1039,6 +1047,7 @@ message NumberStateResponse {
   // If the number does not have a valid state yet.
   // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
   bool missing_state = 3;
+  uint32 device_id = 4;
 }
 message NumberCommandRequest {
   option (id) = 51;
@@ -1080,6 +1089,7 @@ message SelectStateResponse {
   // If the select does not have a valid state yet.
   // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
   bool missing_state = 3;
+  uint32 device_id = 4;
 }
 message SelectCommandRequest {
   option (id) = 54;
@@ -1120,6 +1130,7 @@ message SirenStateResponse {
 
   fixed32 key = 1;
   bool state = 2;
+  uint32 device_id = 3;
 }
 message SirenCommandRequest {
   option (id) = 57;
@@ -1183,6 +1194,7 @@ message LockStateResponse {
   option (no_delay) = true;
   fixed32 key = 1;
   LockState state = 2;
+  uint32 device_id = 3;
 }
 message LockCommandRequest {
   option (id) = 60;
@@ -1282,6 +1294,7 @@ message MediaPlayerStateResponse {
   MediaPlayerState state = 2;
   float volume = 3;
   bool muted = 4;
+  uint32 device_id = 5;
 }
 message MediaPlayerCommandRequest {
   option (id) = 65;
@@ -1822,6 +1835,7 @@ message AlarmControlPanelStateResponse {
   option (no_delay) = true;
   fixed32 key = 1;
   AlarmControlPanelState state = 2;
+  uint32 device_id = 3;
 }
 
 message AlarmControlPanelCommandRequest {
@@ -1871,6 +1885,7 @@ message TextStateResponse {
   // If the Text does not have a valid state yet.
   // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
   bool missing_state = 3;
+  uint32 device_id = 4;
 }
 message TextCommandRequest {
   option (id) = 99;
@@ -1914,6 +1929,7 @@ message DateStateResponse {
   uint32 year = 3;
   uint32 month = 4;
   uint32 day = 5;
+  uint32 device_id = 6;
 }
 message DateCommandRequest {
   option (id) = 102;
@@ -1958,6 +1974,7 @@ message TimeStateResponse {
   uint32 hour = 3;
   uint32 minute = 4;
   uint32 second = 5;
+  uint32 device_id = 6;
 }
 message TimeCommandRequest {
   option (id) = 105;
@@ -1999,6 +2016,7 @@ message EventResponse {
 
   fixed32 key = 1;
   string event_type = 2;
+  uint32 device_id = 3;
 }
 
 // ==================== VALVE ====================
@@ -2039,6 +2057,7 @@ message ValveStateResponse {
   fixed32 key = 1;
   float position = 2;
   ValveOperation current_operation = 3;
+  uint32 device_id = 4;
 }
 
 message ValveCommandRequest {
@@ -2082,6 +2101,7 @@ message DateTimeStateResponse {
   // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
   bool missing_state = 2;
   fixed32 epoch_seconds = 3;
+  uint32 device_id = 4;
 }
 message DateTimeCommandRequest {
   option (id) = 114;
@@ -2128,6 +2148,7 @@ message UpdateStateResponse {
   string title = 8;
   string release_summary = 9;
   string release_url = 10;
+  uint32 device_id = 11;
 }
 enum UpdateCommand {
   UPDATE_COMMAND_NONE = 0;
diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h
index 8922aab94a..dc4b84a535 100644
--- a/esphome/components/api/api_connection.h
+++ b/esphome/components/api/api_connection.h
@@ -292,6 +292,9 @@ class APIConnection : public APIServerConnection {
   // Helper function to fill common entity state fields
   static void fill_entity_state_base(esphome::EntityBase *entity, StateResponseProtoMessage &response) {
     response.key = entity->get_object_id_hash();
+#ifdef USE_DEVICES
+    response.device_id = entity->get_device_id();
+#endif
   }
 
   // Non-template helper to encode any ProtoMessage
diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp
index 01140fbfc8..5c2b22d22a 100644
--- a/esphome/components/api/api_pb2.cpp
+++ b/esphome/components/api/api_pb2.cpp
@@ -417,6 +417,10 @@ bool BinarySensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt val
       this->missing_state = value.as_bool();
       return true;
     }
+    case 4: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -435,11 +439,13 @@ void BinarySensorStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
   buffer.encode_bool(2, this->state);
   buffer.encode_bool(3, this->missing_state);
+  buffer.encode_uint32(4, this->device_id);
 }
 void BinarySensorStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_bool_field(total_size, 1, this->state, false);
   ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 #endif
 #ifdef USE_COVER
@@ -553,6 +559,10 @@ bool CoverStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
       this->current_operation = value.as_enum();
       return true;
     }
+    case 6: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -581,6 +591,7 @@ void CoverStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_float(3, this->position);
   buffer.encode_float(4, this->tilt);
   buffer.encode_enum(5, this->current_operation);
+  buffer.encode_uint32(6, this->device_id);
 }
 void CoverStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
@@ -588,6 +599,7 @@ void CoverStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f, false);
   ProtoSize::add_fixed_field<4>(total_size, 1, this->tilt != 0.0f, false);
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->current_operation), false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
@@ -783,6 +795,10 @@ bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
       this->speed_level = value.as_int32();
       return true;
     }
+    case 8: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -815,6 +831,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_enum(5, this->direction);
   buffer.encode_int32(6, this->speed_level);
   buffer.encode_string(7, this->preset_mode);
+  buffer.encode_uint32(8, this->device_id);
 }
 void FanStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
@@ -824,6 +841,7 @@ void FanStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->direction), false);
   ProtoSize::add_int32_field(total_size, 1, this->speed_level, false);
   ProtoSize::add_string_field(total_size, 1, this->preset_mode, false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
@@ -1067,6 +1085,10 @@ bool LightStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
       this->color_mode = value.as_enum();
       return true;
     }
+    case 14: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -1141,6 +1163,7 @@ void LightStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_float(12, this->cold_white);
   buffer.encode_float(13, this->warm_white);
   buffer.encode_string(9, this->effect);
+  buffer.encode_uint32(14, this->device_id);
 }
 void LightStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
@@ -1156,6 +1179,7 @@ void LightStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->cold_white != 0.0f, false);
   ProtoSize::add_fixed_field<4>(total_size, 1, this->warm_white != 0.0f, false);
   ProtoSize::add_string_field(total_size, 1, this->effect, false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
@@ -1455,6 +1479,10 @@ bool SensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
       this->missing_state = value.as_bool();
       return true;
     }
+    case 4: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -1477,11 +1505,13 @@ void SensorStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
   buffer.encode_float(2, this->state);
   buffer.encode_bool(3, this->missing_state);
+  buffer.encode_uint32(4, this->device_id);
 }
 void SensorStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f, false);
   ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 #endif
 #ifdef USE_SWITCH
@@ -1573,6 +1603,10 @@ bool SwitchStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
       this->state = value.as_bool();
       return true;
     }
+    case 3: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -1590,10 +1624,12 @@ bool SwitchStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
 void SwitchStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
   buffer.encode_bool(2, this->state);
+  buffer.encode_uint32(3, this->device_id);
 }
 void SwitchStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_bool_field(total_size, 1, this->state, false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
@@ -1707,6 +1743,10 @@ bool TextSensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value
       this->missing_state = value.as_bool();
       return true;
     }
+    case 4: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -1735,11 +1775,13 @@ void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
   buffer.encode_string(2, this->state);
   buffer.encode_bool(3, this->missing_state);
+  buffer.encode_uint32(4, this->device_id);
 }
 void TextSensorStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_string_field(total_size, 1, this->state, false);
   ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 #endif
 bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
@@ -2549,6 +2591,10 @@ bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
       this->preset = value.as_enum();
       return true;
     }
+    case 16: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -2617,6 +2663,7 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_string(13, this->custom_preset);
   buffer.encode_float(14, this->current_humidity);
   buffer.encode_float(15, this->target_humidity);
+  buffer.encode_uint32(16, this->device_id);
 }
 void ClimateStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
@@ -2634,6 +2681,7 @@ void ClimateStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->custom_preset, false);
   ProtoSize::add_fixed_field<4>(total_size, 1, this->current_humidity != 0.0f, false);
   ProtoSize::add_fixed_field<4>(total_size, 1, this->target_humidity != 0.0f, false);
+  ProtoSize::add_uint32_field(total_size, 2, this->device_id, false);
 }
 bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
@@ -2909,6 +2957,10 @@ bool NumberStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
       this->missing_state = value.as_bool();
       return true;
     }
+    case 4: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -2931,11 +2983,13 @@ void NumberStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
   buffer.encode_float(2, this->state);
   buffer.encode_bool(3, this->missing_state);
+  buffer.encode_uint32(4, this->device_id);
 }
 void NumberStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f, false);
   ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
   switch (field_id) {
@@ -3049,6 +3103,10 @@ bool SelectStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
       this->missing_state = value.as_bool();
       return true;
     }
+    case 4: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -3077,11 +3135,13 @@ void SelectStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
   buffer.encode_string(2, this->state);
   buffer.encode_bool(3, this->missing_state);
+  buffer.encode_uint32(4, this->device_id);
 }
 void SelectStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_string_field(total_size, 1, this->state, false);
   ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
   switch (field_id) {
@@ -3213,6 +3273,10 @@ bool SirenStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
       this->state = value.as_bool();
       return true;
     }
+    case 3: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -3230,10 +3294,12 @@ bool SirenStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
 void SirenStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
   buffer.encode_bool(2, this->state);
+  buffer.encode_uint32(3, this->device_id);
 }
 void SirenStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_bool_field(total_size, 1, this->state, false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
@@ -3413,6 +3479,10 @@ bool LockStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
       this->state = value.as_enum();
       return true;
     }
+    case 3: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -3430,10 +3500,12 @@ bool LockStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
 void LockStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
   buffer.encode_enum(2, this->state);
+  buffer.encode_uint32(3, this->device_id);
 }
 void LockStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->state), false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
@@ -3715,6 +3787,10 @@ bool MediaPlayerStateResponse::decode_varint(uint32_t field_id, ProtoVarInt valu
       this->muted = value.as_bool();
       return true;
     }
+    case 5: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -3738,12 +3814,14 @@ void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_enum(2, this->state);
   buffer.encode_float(3, this->volume);
   buffer.encode_bool(4, this->muted);
+  buffer.encode_uint32(5, this->device_id);
 }
 void MediaPlayerStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->state), false);
   ProtoSize::add_fixed_field<4>(total_size, 1, this->volume != 0.0f, false);
   ProtoSize::add_bool_field(total_size, 1, this->muted, false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
@@ -5199,6 +5277,10 @@ bool AlarmControlPanelStateResponse::decode_varint(uint32_t field_id, ProtoVarIn
       this->state = value.as_enum();
       return true;
     }
+    case 3: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -5216,10 +5298,12 @@ bool AlarmControlPanelStateResponse::decode_32bit(uint32_t field_id, Proto32Bit
 void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
   buffer.encode_enum(2, this->state);
+  buffer.encode_uint32(3, this->device_id);
 }
 void AlarmControlPanelStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->state), false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
@@ -5363,6 +5447,10 @@ bool TextStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
       this->missing_state = value.as_bool();
       return true;
     }
+    case 4: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -5391,11 +5479,13 @@ void TextStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
   buffer.encode_string(2, this->state);
   buffer.encode_bool(3, this->missing_state);
+  buffer.encode_uint32(4, this->device_id);
 }
 void TextStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_string_field(total_size, 1, this->state, false);
   ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 bool TextCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
   switch (field_id) {
@@ -5515,6 +5605,10 @@ bool DateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
       this->day = value.as_uint32();
       return true;
     }
+    case 6: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -5535,6 +5629,7 @@ void DateStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_uint32(3, this->year);
   buffer.encode_uint32(4, this->month);
   buffer.encode_uint32(5, this->day);
+  buffer.encode_uint32(6, this->device_id);
 }
 void DateStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
@@ -5542,6 +5637,7 @@ void DateStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint32_field(total_size, 1, this->year, false);
   ProtoSize::add_uint32_field(total_size, 1, this->month, false);
   ProtoSize::add_uint32_field(total_size, 1, this->day, false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 bool DateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
@@ -5673,6 +5769,10 @@ bool TimeStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
       this->second = value.as_uint32();
       return true;
     }
+    case 6: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -5693,6 +5793,7 @@ void TimeStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_uint32(3, this->hour);
   buffer.encode_uint32(4, this->minute);
   buffer.encode_uint32(5, this->second);
+  buffer.encode_uint32(6, this->device_id);
 }
 void TimeStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
@@ -5700,6 +5801,7 @@ void TimeStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_uint32_field(total_size, 1, this->hour, false);
   ProtoSize::add_uint32_field(total_size, 1, this->minute, false);
   ProtoSize::add_uint32_field(total_size, 1, this->second, false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 bool TimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
@@ -5831,6 +5933,16 @@ void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const {
   }
   ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
+bool EventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 3: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
 bool EventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
   switch (field_id) {
     case 2: {
@@ -5854,10 +5966,12 @@ bool EventResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
 void EventResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
   buffer.encode_string(2, this->event_type);
+  buffer.encode_uint32(3, this->device_id);
 }
 void EventResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_string_field(total_size, 1, this->event_type, false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 #endif
 #ifdef USE_VALVE
@@ -5961,6 +6075,10 @@ bool ValveStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
       this->current_operation = value.as_enum();
       return true;
     }
+    case 4: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -5983,11 +6101,13 @@ void ValveStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
   buffer.encode_float(2, this->position);
   buffer.encode_enum(3, this->current_operation);
+  buffer.encode_uint32(4, this->device_id);
 }
 void ValveStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f, false);
   ProtoSize::add_enum_field(total_size, 1, static_cast(this->current_operation), false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 bool ValveCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
@@ -6107,6 +6227,10 @@ bool DateTimeStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value)
       this->missing_state = value.as_bool();
       return true;
     }
+    case 4: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -6129,11 +6253,13 @@ void DateTimeStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
   buffer.encode_bool(2, this->missing_state);
   buffer.encode_fixed32(3, this->epoch_seconds);
+  buffer.encode_uint32(4, this->device_id);
 }
 void DateTimeStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
   ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0, false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
   switch (field_id) {
@@ -6249,6 +6375,10 @@ bool UpdateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
       this->has_progress = value.as_bool();
       return true;
     }
+    case 11: {
+      this->device_id = value.as_uint32();
+      return true;
+    }
     default:
       return false;
   }
@@ -6304,6 +6434,7 @@ void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_string(8, this->title);
   buffer.encode_string(9, this->release_summary);
   buffer.encode_string(10, this->release_url);
+  buffer.encode_uint32(11, this->device_id);
 }
 void UpdateStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
@@ -6316,6 +6447,7 @@ void UpdateStateResponse::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_string_field(total_size, 1, this->title, false);
   ProtoSize::add_string_field(total_size, 1, this->release_summary, false);
   ProtoSize::add_string_field(total_size, 1, this->release_url, false);
+  ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
 }
 bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h
index 24b0e891c9..c0079bd29c 100644
--- a/esphome/components/api/api_pb2.h
+++ b/esphome/components/api/api_pb2.h
@@ -303,6 +303,7 @@ class StateResponseProtoMessage : public ProtoMessage {
  public:
   ~StateResponseProtoMessage() override = default;
   uint32_t key{0};
+  uint32_t device_id{0};
 
  protected:
 };
@@ -577,7 +578,7 @@ class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage {
 class BinarySensorStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 21;
-  static constexpr uint16_t ESTIMATED_SIZE = 9;
+  static constexpr uint16_t ESTIMATED_SIZE = 13;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "binary_sensor_state_response"; }
 #endif
@@ -621,7 +622,7 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage {
 class CoverStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 22;
-  static constexpr uint16_t ESTIMATED_SIZE = 19;
+  static constexpr uint16_t ESTIMATED_SIZE = 23;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "cover_state_response"; }
 #endif
@@ -692,7 +693,7 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage {
 class FanStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 23;
-  static constexpr uint16_t ESTIMATED_SIZE = 26;
+  static constexpr uint16_t ESTIMATED_SIZE = 30;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "fan_state_response"; }
 #endif
@@ -775,7 +776,7 @@ class ListEntitiesLightResponse : public InfoResponseProtoMessage {
 class LightStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 24;
-  static constexpr uint16_t ESTIMATED_SIZE = 63;
+  static constexpr uint16_t ESTIMATED_SIZE = 67;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "light_state_response"; }
 #endif
@@ -876,7 +877,7 @@ class ListEntitiesSensorResponse : public InfoResponseProtoMessage {
 class SensorStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 25;
-  static constexpr uint16_t ESTIMATED_SIZE = 12;
+  static constexpr uint16_t ESTIMATED_SIZE = 16;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "sensor_state_response"; }
 #endif
@@ -917,7 +918,7 @@ class ListEntitiesSwitchResponse : public InfoResponseProtoMessage {
 class SwitchStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 26;
-  static constexpr uint16_t ESTIMATED_SIZE = 7;
+  static constexpr uint16_t ESTIMATED_SIZE = 11;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "switch_state_response"; }
 #endif
@@ -975,7 +976,7 @@ class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage {
 class TextSensorStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 27;
-  static constexpr uint16_t ESTIMATED_SIZE = 16;
+  static constexpr uint16_t ESTIMATED_SIZE = 20;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "text_sensor_state_response"; }
 #endif
@@ -1371,7 +1372,7 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
 class ClimateStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 47;
-  static constexpr uint16_t ESTIMATED_SIZE = 65;
+  static constexpr uint16_t ESTIMATED_SIZE = 70;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "climate_state_response"; }
 #endif
@@ -1470,7 +1471,7 @@ class ListEntitiesNumberResponse : public InfoResponseProtoMessage {
 class NumberStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 50;
-  static constexpr uint16_t ESTIMATED_SIZE = 12;
+  static constexpr uint16_t ESTIMATED_SIZE = 16;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "number_state_response"; }
 #endif
@@ -1528,7 +1529,7 @@ class ListEntitiesSelectResponse : public InfoResponseProtoMessage {
 class SelectStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 53;
-  static constexpr uint16_t ESTIMATED_SIZE = 16;
+  static constexpr uint16_t ESTIMATED_SIZE = 20;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "select_state_response"; }
 #endif
@@ -1590,7 +1591,7 @@ class ListEntitiesSirenResponse : public InfoResponseProtoMessage {
 class SirenStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 56;
-  static constexpr uint16_t ESTIMATED_SIZE = 7;
+  static constexpr uint16_t ESTIMATED_SIZE = 11;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "siren_state_response"; }
 #endif
@@ -1659,7 +1660,7 @@ class ListEntitiesLockResponse : public InfoResponseProtoMessage {
 class LockStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 59;
-  static constexpr uint16_t ESTIMATED_SIZE = 7;
+  static constexpr uint16_t ESTIMATED_SIZE = 11;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "lock_state_response"; }
 #endif
@@ -1776,7 +1777,7 @@ class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage {
 class MediaPlayerStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 64;
-  static constexpr uint16_t ESTIMATED_SIZE = 14;
+  static constexpr uint16_t ESTIMATED_SIZE = 18;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "media_player_state_response"; }
 #endif
@@ -2653,7 +2654,7 @@ class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage {
 class AlarmControlPanelStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 95;
-  static constexpr uint16_t ESTIMATED_SIZE = 7;
+  static constexpr uint16_t ESTIMATED_SIZE = 11;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "alarm_control_panel_state_response"; }
 #endif
@@ -2716,7 +2717,7 @@ class ListEntitiesTextResponse : public InfoResponseProtoMessage {
 class TextStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 98;
-  static constexpr uint16_t ESTIMATED_SIZE = 16;
+  static constexpr uint16_t ESTIMATED_SIZE = 20;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "text_state_response"; }
 #endif
@@ -2775,7 +2776,7 @@ class ListEntitiesDateResponse : public InfoResponseProtoMessage {
 class DateStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 101;
-  static constexpr uint16_t ESTIMATED_SIZE = 19;
+  static constexpr uint16_t ESTIMATED_SIZE = 23;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "date_state_response"; }
 #endif
@@ -2837,7 +2838,7 @@ class ListEntitiesTimeResponse : public InfoResponseProtoMessage {
 class TimeStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 104;
-  static constexpr uint16_t ESTIMATED_SIZE = 19;
+  static constexpr uint16_t ESTIMATED_SIZE = 23;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "time_state_response"; }
 #endif
@@ -2901,7 +2902,7 @@ class ListEntitiesEventResponse : public InfoResponseProtoMessage {
 class EventResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 108;
-  static constexpr uint16_t ESTIMATED_SIZE = 14;
+  static constexpr uint16_t ESTIMATED_SIZE = 18;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "event_response"; }
 #endif
@@ -2915,6 +2916,7 @@ class EventResponse : public StateResponseProtoMessage {
  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, ProtoVarInt value) override;
 };
 #endif
 #ifdef USE_VALVE
@@ -2943,7 +2945,7 @@ class ListEntitiesValveResponse : public InfoResponseProtoMessage {
 class ValveStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 110;
-  static constexpr uint16_t ESTIMATED_SIZE = 12;
+  static constexpr uint16_t ESTIMATED_SIZE = 16;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "valve_state_response"; }
 #endif
@@ -3003,7 +3005,7 @@ class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage {
 class DateTimeStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 113;
-  static constexpr uint16_t ESTIMATED_SIZE = 12;
+  static constexpr uint16_t ESTIMATED_SIZE = 16;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "date_time_state_response"; }
 #endif
@@ -3061,7 +3063,7 @@ class ListEntitiesUpdateResponse : public InfoResponseProtoMessage {
 class UpdateStateResponse : public StateResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 117;
-  static constexpr uint16_t ESTIMATED_SIZE = 61;
+  static constexpr uint16_t ESTIMATED_SIZE = 65;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   const char *message_name() const override { return "update_state_response"; }
 #endif
diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp
index 6658fd754b..db330a17fb 100644
--- a/esphome/components/api/api_pb2_dump.cpp
+++ b/esphome/components/api/api_pb2_dump.cpp
@@ -850,6 +850,11 @@ void BinarySensorStateResponse::dump_to(std::string &out) const {
   out.append("  missing_state: ");
   out.append(YESNO(this->missing_state));
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 #endif
@@ -937,6 +942,11 @@ void CoverStateResponse::dump_to(std::string &out) const {
   out.append("  current_operation: ");
   out.append(proto_enum_to_string(this->current_operation));
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 void CoverCommandRequest::dump_to(std::string &out) const {
@@ -1073,6 +1083,11 @@ void FanStateResponse::dump_to(std::string &out) const {
   out.append("  preset_mode: ");
   out.append("'").append(this->preset_mode).append("'");
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 void FanCommandRequest::dump_to(std::string &out) const {
@@ -1275,6 +1290,11 @@ void LightStateResponse::dump_to(std::string &out) const {
   out.append("  effect: ");
   out.append("'").append(this->effect).append("'");
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 void LightCommandRequest::dump_to(std::string &out) const {
@@ -1482,6 +1502,11 @@ void SensorStateResponse::dump_to(std::string &out) const {
   out.append("  missing_state: ");
   out.append(YESNO(this->missing_state));
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 #endif
@@ -1543,6 +1568,11 @@ void SwitchStateResponse::dump_to(std::string &out) const {
   out.append("  state: ");
   out.append(YESNO(this->state));
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 void SwitchCommandRequest::dump_to(std::string &out) const {
@@ -1617,6 +1647,11 @@ void TextSensorStateResponse::dump_to(std::string &out) const {
   out.append("  missing_state: ");
   out.append(YESNO(this->missing_state));
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 #endif
@@ -2122,6 +2157,11 @@ void ClimateStateResponse::dump_to(std::string &out) const {
   sprintf(buffer, "%g", this->target_humidity);
   out.append(buffer);
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 void ClimateCommandRequest::dump_to(std::string &out) const {
@@ -2308,6 +2348,11 @@ void NumberStateResponse::dump_to(std::string &out) const {
   out.append("  missing_state: ");
   out.append(YESNO(this->missing_state));
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 void NumberCommandRequest::dump_to(std::string &out) const {
@@ -2385,6 +2430,11 @@ void SelectStateResponse::dump_to(std::string &out) const {
   out.append("  missing_state: ");
   out.append(YESNO(this->missing_state));
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 void SelectCommandRequest::dump_to(std::string &out) const {
@@ -2465,6 +2515,11 @@ void SirenStateResponse::dump_to(std::string &out) const {
   out.append("  state: ");
   out.append(YESNO(this->state));
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 void SirenCommandRequest::dump_to(std::string &out) const {
@@ -2577,6 +2632,11 @@ void LockStateResponse::dump_to(std::string &out) const {
   out.append("  state: ");
   out.append(proto_enum_to_string(this->state));
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 void LockCommandRequest::dump_to(std::string &out) const {
@@ -2750,6 +2810,11 @@ void MediaPlayerStateResponse::dump_to(std::string &out) const {
   out.append("  muted: ");
   out.append(YESNO(this->muted));
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 void MediaPlayerCommandRequest::dump_to(std::string &out) const {
@@ -3595,6 +3660,11 @@ void AlarmControlPanelStateResponse::dump_to(std::string &out) const {
   out.append("  state: ");
   out.append(proto_enum_to_string(this->state));
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 void AlarmControlPanelCommandRequest::dump_to(std::string &out) const {
@@ -3687,6 +3757,11 @@ void TextStateResponse::dump_to(std::string &out) const {
   out.append("  missing_state: ");
   out.append(YESNO(this->missing_state));
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 void TextCommandRequest::dump_to(std::string &out) const {
@@ -3768,6 +3843,11 @@ void DateStateResponse::dump_to(std::string &out) const {
   sprintf(buffer, "%" PRIu32, this->day);
   out.append(buffer);
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 void DateCommandRequest::dump_to(std::string &out) const {
@@ -3860,6 +3940,11 @@ void TimeStateResponse::dump_to(std::string &out) const {
   sprintf(buffer, "%" PRIu32, this->second);
   out.append(buffer);
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 void TimeCommandRequest::dump_to(std::string &out) const {
@@ -3947,6 +4032,11 @@ void EventResponse::dump_to(std::string &out) const {
   out.append("  event_type: ");
   out.append("'").append(this->event_type).append("'");
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 #endif
@@ -4021,6 +4111,11 @@ void ValveStateResponse::dump_to(std::string &out) const {
   out.append("  current_operation: ");
   out.append(proto_enum_to_string(this->current_operation));
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 void ValveCommandRequest::dump_to(std::string &out) const {
@@ -4101,6 +4196,11 @@ void DateTimeStateResponse::dump_to(std::string &out) const {
   sprintf(buffer, "%" PRIu32, this->epoch_seconds);
   out.append(buffer);
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 void DateTimeCommandRequest::dump_to(std::string &out) const {
@@ -4205,6 +4305,11 @@ void UpdateStateResponse::dump_to(std::string &out) const {
   out.append("  release_url: ");
   out.append("'").append(this->release_url).append("'");
   out.append("\n");
+
+  out.append("  device_id: ");
+  sprintf(buffer, "%" PRIu32, this->device_id);
+  out.append(buffer);
+  out.append("\n");
   out.append("}");
 }
 void UpdateCommandRequest::dump_to(std::string &out) const {
diff --git a/tests/integration/fixtures/device_id_in_state.yaml b/tests/integration/fixtures/device_id_in_state.yaml
new file mode 100644
index 0000000000..f2e320a2e2
--- /dev/null
+++ b/tests/integration/fixtures/device_id_in_state.yaml
@@ -0,0 +1,85 @@
+esphome:
+  name: device-id-state-test
+  # Define areas
+  areas:
+    - id: living_room
+      name: Living Room
+    - id: bedroom
+      name: Bedroom
+  # Define devices
+  devices:
+    - id: temperature_monitor
+      name: Temperature Monitor
+      area_id: living_room
+    - id: humidity_monitor
+      name: Humidity Monitor
+      area_id: bedroom
+    - id: motion_sensor
+      name: Motion Sensor
+      area_id: living_room
+
+host:
+api:
+logger:
+
+# Test different entity types with device assignments
+sensor:
+  - platform: template
+    name: Temperature
+    device_id: temperature_monitor
+    lambda: return 25.5;
+    update_interval: 0.1s
+    unit_of_measurement: "°C"
+
+  - platform: template
+    name: Humidity
+    device_id: humidity_monitor
+    lambda: return 65.0;
+    update_interval: 0.1s
+    unit_of_measurement: "%"
+
+  # Test entity without device_id (should have device_id 0)
+  - platform: template
+    name: No Device Sensor
+    lambda: return 100.0;
+    update_interval: 0.1s
+
+binary_sensor:
+  - platform: template
+    name: Motion Detected
+    device_id: motion_sensor
+    lambda: return true;
+
+switch:
+  - platform: template
+    name: Temperature Monitor Power
+    device_id: temperature_monitor
+    lambda: return true;
+    turn_on_action:
+      - lambda: |-
+          ESP_LOGD("test", "Turning on");
+    turn_off_action:
+      - lambda: |-
+          ESP_LOGD("test", "Turning off");
+
+text_sensor:
+  - platform: template
+    name: Temperature Status
+    device_id: temperature_monitor
+    lambda: return {"Normal"};
+    update_interval: 0.1s
+
+light:
+  - platform: binary
+    name: Motion Light
+    device_id: motion_sensor
+    output: motion_light_output
+
+output:
+  - platform: template
+    id: motion_light_output
+    type: binary
+    write_action:
+      - lambda: |-
+          ESP_LOGD("test", "Light output: %d", state);
+
diff --git a/tests/integration/test_device_id_in_state.py b/tests/integration/test_device_id_in_state.py
new file mode 100644
index 0000000000..3c5181595f
--- /dev/null
+++ b/tests/integration/test_device_id_in_state.py
@@ -0,0 +1,161 @@
+"""Integration test for device_id in entity state responses."""
+
+from __future__ import annotations
+
+import asyncio
+
+from aioesphomeapi import EntityState
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_device_id_in_state(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that device_id is included in entity state responses."""
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Get device info to verify devices are configured
+        device_info = await client.device_info()
+        assert device_info is not None
+
+        # Verify devices exist
+        devices = device_info.devices
+        assert len(devices) >= 3, f"Expected at least 3 devices, got {len(devices)}"
+
+        # Get device IDs for verification
+        device_ids = {device.name: device.device_id for device in devices}
+        assert "Temperature Monitor" in device_ids
+        assert "Humidity Monitor" in device_ids
+        assert "Motion Sensor" in device_ids
+
+        # Get entity list
+        entities = await client.list_entities_services()
+        all_entities = entities[0]
+
+        # Create a mapping of entity key to expected device_id
+        entity_device_mapping: dict[int, int] = {}
+
+        for entity in all_entities:
+            if hasattr(entity, "name") and hasattr(entity, "key"):
+                if entity.name == "Temperature":
+                    entity_device_mapping[entity.key] = device_ids[
+                        "Temperature Monitor"
+                    ]
+                elif entity.name == "Humidity":
+                    entity_device_mapping[entity.key] = device_ids["Humidity Monitor"]
+                elif entity.name == "Motion Detected":
+                    entity_device_mapping[entity.key] = device_ids["Motion Sensor"]
+                elif entity.name == "Temperature Monitor Power":
+                    entity_device_mapping[entity.key] = device_ids[
+                        "Temperature Monitor"
+                    ]
+                elif entity.name == "Temperature Status":
+                    entity_device_mapping[entity.key] = device_ids[
+                        "Temperature Monitor"
+                    ]
+                elif entity.name == "Motion Light":
+                    entity_device_mapping[entity.key] = device_ids["Motion Sensor"]
+                elif entity.name == "No Device Sensor":
+                    # Entity without device_id should have device_id 0
+                    entity_device_mapping[entity.key] = 0
+
+        assert len(entity_device_mapping) >= 6, (
+            f"Expected at least 6 mapped entities, got {len(entity_device_mapping)}"
+        )
+
+        # Subscribe to states
+        loop = asyncio.get_running_loop()
+        states: dict[int, EntityState] = {}
+        states_future: asyncio.Future[bool] = loop.create_future()
+
+        def on_state(state: EntityState) -> None:
+            states[state.key] = state
+            # Check if we have states for all mapped entities
+            if len(states) >= len(entity_device_mapping) and not states_future.done():
+                states_future.set_result(True)
+
+        client.subscribe_states(on_state)
+
+        # Wait for states
+        try:
+            await asyncio.wait_for(states_future, timeout=10.0)
+        except asyncio.TimeoutError:
+            pytest.fail(
+                f"Did not receive all entity states within 10 seconds. "
+                f"Received {len(states)} states, expected {len(entity_device_mapping)}"
+            )
+
+        # Verify each state has the correct device_id
+        verified_count = 0
+        for key, expected_device_id in entity_device_mapping.items():
+            if key in states:
+                state = states[key]
+
+                assert state.device_id == expected_device_id, (
+                    f"State for key {key} has device_id {state.device_id}, "
+                    f"expected {expected_device_id}"
+                )
+                verified_count += 1
+
+        assert verified_count >= 6, (
+            f"Only verified {verified_count} states, expected at least 6"
+        )
+
+        # Test specific state types to ensure device_id is present
+        # Find a sensor state with device_id
+        sensor_state = next(
+            (
+                s
+                for s in states.values()
+                if hasattr(s, "state")
+                and isinstance(s.state, float)
+                and s.device_id != 0
+            ),
+            None,
+        )
+        assert sensor_state is not None, "No sensor state with device_id found"
+        assert sensor_state.device_id > 0, "Sensor state should have non-zero device_id"
+
+        # Find a binary sensor state
+        binary_sensor_state = next(
+            (
+                s
+                for s in states.values()
+                if hasattr(s, "state") and isinstance(s.state, bool)
+            ),
+            None,
+        )
+        assert binary_sensor_state is not None, "No binary sensor state found"
+        assert binary_sensor_state.device_id > 0, (
+            "Binary sensor state should have non-zero device_id"
+        )
+
+        # Find a text sensor state
+        text_sensor_state = next(
+            (
+                s
+                for s in states.values()
+                if hasattr(s, "state") and isinstance(s.state, str)
+            ),
+            None,
+        )
+        assert text_sensor_state is not None, "No text sensor state found"
+        assert text_sensor_state.device_id > 0, (
+            "Text sensor state should have non-zero device_id"
+        )
+
+        # Verify the "No Device Sensor" has device_id = 0
+        no_device_key = next(
+            (key for key, device_id in entity_device_mapping.items() if device_id == 0),
+            None,
+        )
+        assert no_device_key is not None, "No entity mapped to device_id 0"
+        assert no_device_key in states, f"State for key {no_device_key} not found"
+        no_device_state = states[no_device_key]
+        assert no_device_state.device_id == 0, (
+            f"Entity without device_id should have device_id=0, got {no_device_state.device_id}"
+        )

From 25457da97c031173e6b7f3749cc29d1b14d47a0b Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Thu, 3 Jul 2025 19:33:19 -0500
Subject: [PATCH 227/293] Fix web_server URL parsing lifetime issue (#9309)

---
 esphome/components/web_server/web_server.cpp | 85 +++++++++-----------
 1 file changed, 40 insertions(+), 45 deletions(-)

diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp
index d5ded2a02c..1242db57ff 100644
--- a/esphome/components/web_server/web_server.cpp
+++ b/esphome/components/web_server/web_server.cpp
@@ -46,70 +46,58 @@ static const char *const HEADER_CORS_REQ_PNA = "Access-Control-Request-Private-N
 static const char *const HEADER_CORS_ALLOW_PNA = "Access-Control-Allow-Private-Network";
 #endif
 
-UrlMatch match_url(const std::string &url, bool only_domain = false) {
-  UrlMatch match;
-  match.valid = false;
-  match.domain = nullptr;
-  match.id = nullptr;
-  match.method = nullptr;
-  match.domain_len = 0;
-  match.id_len = 0;
-  match.method_len = 0;
-
-  const char *url_ptr = url.c_str();
-  size_t url_len = url.length();
+// Parse URL and return match info
+static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain) {
+  UrlMatch match{};
 
   // URL must start with '/'
-  if (url_len < 2 || url_ptr[0] != '/')
+  if (url_len < 2 || url_ptr[0] != '/') {
     return match;
+  }
 
-  // Find domain
-  size_t domain_start = 1;
-  size_t domain_end = url.find('/', domain_start);
+  // Skip leading '/'
+  const char *start = url_ptr + 1;
+  const char *end = url_ptr + url_len;
 
-  if (domain_end == std::string::npos) {
-    // URL is just "/domain"
-    match.domain = url_ptr + domain_start;
-    match.domain_len = url_len - domain_start;
-    match.valid = true;
+  // Find domain (everything up to next '/' or end)
+  const char *domain_end = (const char *) memchr(start, '/', end - start);
+  if (!domain_end) {
+    // No second slash found - original behavior returns invalid
     return match;
   }
 
   // Set domain
-  match.domain = url_ptr + domain_start;
-  match.domain_len = domain_end - domain_start;
+  match.domain = start;
+  match.domain_len = domain_end - start;
+  match.valid = true;
 
   if (only_domain) {
-    match.valid = true;
     return match;
   }
 
-  // Check if there's anything after domain
-  if (url_len == domain_end + 1)
-    return match;
+  // Parse ID if present
+  if (domain_end + 1 >= end) {
+    return match;  // Nothing after domain slash
+  }
 
-  // Find ID
-  size_t id_begin = domain_end + 1;
-  size_t id_end = url.find('/', id_begin);
+  const char *id_start = domain_end + 1;
+  const char *id_end = (const char *) memchr(id_start, '/', end - id_start);
 
-  match.valid = true;
-
-  if (id_end == std::string::npos) {
-    // URL is "/domain/id" with no method
-    match.id = url_ptr + id_begin;
-    match.id_len = url_len - id_begin;
+  if (!id_end) {
+    // No more slashes, entire remaining string is ID
+    match.id = id_start;
+    match.id_len = end - id_start;
     return match;
   }
 
   // Set ID
-  match.id = url_ptr + id_begin;
-  match.id_len = id_end - id_begin;
+  match.id = id_start;
+  match.id_len = id_end - id_start;
 
-  // Set method if present
-  size_t method_begin = id_end + 1;
-  if (method_begin < url_len) {
-    match.method = url_ptr + method_begin;
-    match.method_len = url_len - method_begin;
+  // Parse method if present
+  if (id_end + 1 < end) {
+    match.method = id_end + 1;
+    match.method_len = end - (id_end + 1);
   }
 
   return match;
@@ -1759,7 +1747,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const {
   }
 #endif
 
-  UrlMatch match = match_url(request->url().c_str(), true);  // NOLINT
+  // Store the URL to prevent temporary string destruction
+  // request->url() returns a reference to a String (on Arduino) or std::string (on ESP-IDF)
+  // UrlMatch stores pointers to the string's data, so we must ensure the string outlives match_url()
+  const auto &url = request->url();
+  UrlMatch match = match_url(url.c_str(), url.length(), true);
   if (!match.valid)
     return false;
 #ifdef USE_SENSOR
@@ -1898,7 +1890,10 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
   }
 #endif
 
-  UrlMatch match = match_url(request->url().c_str());  // NOLINT
+  // See comment in canHandle() for why we store the URL reference
+  const auto &url = request->url();
+  UrlMatch match = match_url(url.c_str(), url.length(), false);
+
 #ifdef USE_SENSOR
   if (match.domain_equals("sensor")) {
     this->handle_sensor_request(request, match);

From d00e20ccdf0b2c9e1e82424f60f3814908ac7fbf Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Thu, 3 Jul 2025 19:53:14 -0500
Subject: [PATCH 228/293] Reduce web_server loop overhead on ESP32 by avoiding
 unnecessary semaphore operations (#9308)

---
 esphome/components/web_server/web_server.cpp | 7 ++++++-
 esphome/components/web_server/web_server.h   | 2 ++
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp
index 1242db57ff..f576507c0f 100644
--- a/esphome/components/web_server/web_server.cpp
+++ b/esphome/components/web_server/web_server.cpp
@@ -299,7 +299,8 @@ void WebServer::setup() {
 }
 void WebServer::loop() {
 #ifdef USE_ESP32
-  if (xSemaphoreTake(this->to_schedule_lock_, 0L)) {
+  // Check atomic flag first to avoid taking semaphore when queue is empty
+  if (this->to_schedule_has_items_.load(std::memory_order_relaxed) && xSemaphoreTake(this->to_schedule_lock_, 0L)) {
     std::function fn;
     if (!to_schedule_.empty()) {
       // scheduler execute things out of order which may lead to incorrect state
@@ -307,6 +308,9 @@ void WebServer::loop() {
       // let's execute it directly from the loop
       fn = std::move(to_schedule_.front());
       to_schedule_.pop_front();
+      if (to_schedule_.empty()) {
+        this->to_schedule_has_items_.store(false, std::memory_order_relaxed);
+      }
     }
     xSemaphoreGive(this->to_schedule_lock_);
     if (fn) {
@@ -2061,6 +2065,7 @@ void WebServer::schedule_(std::function &&f) {
 #ifdef USE_ESP32
   xSemaphoreTake(this->to_schedule_lock_, portMAX_DELAY);
   to_schedule_.push_back(std::move(f));
+  this->to_schedule_has_items_.store(true, std::memory_order_relaxed);
   xSemaphoreGive(this->to_schedule_lock_);
 #else
   this->defer(std::move(f));
diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h
index 5f175b6bdd..c654d83bbd 100644
--- a/esphome/components/web_server/web_server.h
+++ b/esphome/components/web_server/web_server.h
@@ -18,6 +18,7 @@
 #include 
 #include 
 #include 
+#include 
 #endif
 
 #if USE_WEBSERVER_VERSION >= 2
@@ -524,6 +525,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
 #ifdef USE_ESP32
   std::deque> to_schedule_;
   SemaphoreHandle_t to_schedule_lock_;
+  std::atomic to_schedule_has_items_{false};
 #endif
 };
 

From adb7ccdbc7b4645447144db41c9aa21dfddb6bf5 Mon Sep 17 00:00:00 2001
From: Big Mike 
Date: Thu, 3 Jul 2025 20:00:50 -0500
Subject: [PATCH 229/293] Fix compiler warning in tsl2591 component (#9310)

---
 esphome/components/tsl2591/tsl2591.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp
index 1734d83dd2..c7622b116a 100644
--- a/esphome/components/tsl2591/tsl2591.cpp
+++ b/esphome/components/tsl2591/tsl2591.cpp
@@ -232,7 +232,7 @@ void TSL2591Component::set_integration_time_and_gain(TSL2591IntegrationTime inte
   this->integration_time_ = integration_time;
   this->gain_ = gain;
   if (!this->write_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CONTROL,
-                        this->integration_time_ | this->gain_)) {  // NOLINT
+                        static_cast(this->integration_time_) | static_cast(this->gain_))) {
     ESP_LOGE(TAG, "I2C write failed");
   }
   // The ADC values can be confused if gain or integration time are changed in the middle of a cycle.

From d686257cfff4d2e22046bf0ac3b609b120257495 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Thu, 3 Jul 2025 21:07:25 -0500
Subject: [PATCH 230/293] Fix web_server busy loop with ungracefully
 disconnected clients (#9312)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/web_server/web_server.cpp | 11 +++++++++++
 esphome/components/web_server/web_server.h   |  2 ++
 2 files changed, 13 insertions(+)

diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp
index f576507c0f..77c20b956b 100644
--- a/esphome/components/web_server/web_server.cpp
+++ b/esphome/components/web_server/web_server.cpp
@@ -125,7 +125,16 @@ void DeferredUpdateEventSource::process_deferred_queue_() {
     if (this->send(message.c_str(), "state") != DISCARDED) {
       // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
       deferred_queue_.erase(deferred_queue_.begin());
+      this->consecutive_send_failures_ = 0;  // Reset failure count on successful send
     } else {
+      this->consecutive_send_failures_++;
+      if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) {
+        // Too many failures, connection is likely dead
+        ESP_LOGW(TAG, "Closing stuck EventSource connection after %" PRIu16 " failed sends",
+                 this->consecutive_send_failures_);
+        this->close();
+        this->deferred_queue_.clear();
+      }
       break;
     }
   }
@@ -164,6 +173,8 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char *
     std::string message = message_generator(web_server_, source);
     if (this->send(message.c_str(), "state") == DISCARDED) {
       deq_push_back_with_dedup_(source, message_generator);
+    } else {
+      this->consecutive_send_failures_ = 0;  // Reset failure count on successful send
     }
   }
 }
diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h
index c654d83bbd..82b31ab656 100644
--- a/esphome/components/web_server/web_server.h
+++ b/esphome/components/web_server/web_server.h
@@ -127,6 +127,8 @@ class DeferredUpdateEventSource : public AsyncEventSource {
   // footprint is more important than speed here)
   std::vector deferred_queue_;
   WebServer *web_server_;
+  uint16_t consecutive_send_failures_{0};
+  static constexpr uint16_t MAX_CONSECUTIVE_SEND_FAILURES = 2500;  // ~20 seconds at 125Hz loop rate
 
   // helper for allowing only unique entries in the queue
   void deq_push_back_with_dedup_(void *source, message_generator_t *message_generator);

From 58b4e7dab2791e2cec7b87ad7fae015989481044 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 4 Jul 2025 20:54:46 +0000
Subject: [PATCH 231/293] Bump puremagic from 1.29 to 1.30 (#9320)

---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index c4dae9792d..a6bcebaeea 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -15,7 +15,7 @@ click==8.1.7
 esphome-dashboard==20250514.0
 aioesphomeapi==34.1.0
 zeroconf==0.147.0
-puremagic==1.29
+puremagic==1.30
 ruamel.yaml==0.18.14 # dashboard_import
 esphome-glyphsets==0.2.0
 pillow==10.4.0

From 86e7013f406487ff4da5945364efbe85fdd55d19 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Fri, 4 Jul 2025 21:52:12 -0500
Subject: [PATCH 232/293] Add const char overload for Component::defer()
 (#9324)

---
 esphome/core/component.cpp                    |  3 ++
 esphome/core/component.h                      | 15 +++++++
 .../fixtures/scheduler_string_test.yaml       | 43 +++++++++++++++++--
 .../integration/test_scheduler_string_test.py | 38 +++++++++++++++-
 4 files changed, 95 insertions(+), 4 deletions(-)

diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp
index aba5dc729c..9ef30081aa 100644
--- a/esphome/core/component.cpp
+++ b/esphome/core/component.cpp
@@ -248,6 +248,9 @@ bool Component::cancel_defer(const std::string &name) {  // NOLINT
 void Component::defer(const std::string &name, std::function &&f) {  // NOLINT
   App.scheduler.set_timeout(this, name, 0, std::move(f));
 }
+void Component::defer(const char *name, std::function &&f) {  // NOLINT
+  App.scheduler.set_timeout(this, name, 0, std::move(f));
+}
 void Component::set_timeout(uint32_t timeout, std::function &&f) {  // NOLINT
   App.scheduler.set_timeout(this, "", timeout, std::move(f));
 }
diff --git a/esphome/core/component.h b/esphome/core/component.h
index ab30466e2d..3734473a02 100644
--- a/esphome/core/component.h
+++ b/esphome/core/component.h
@@ -380,6 +380,21 @@ class Component {
    */
   void defer(const std::string &name, std::function &&f);  // NOLINT
 
+  /** Defer a callback to the next loop() call with a const char* name.
+   *
+   * IMPORTANT: The provided name pointer must remain valid for the lifetime of the deferred task.
+   * This means the name should be:
+   *   - A string literal (e.g., "update")
+   *   - A static const char* variable
+   *   - A pointer with lifetime >= the deferred execution
+   *
+   * For dynamic strings, use the std::string overload instead.
+   *
+   * @param name The name of the defer function (must have static lifetime)
+   * @param f The callback
+   */
+  void defer(const char *name, std::function &&f);  // NOLINT
+
   /// Defer a callback to the next loop() call.
   void defer(std::function &&f);  // NOLINT
 
diff --git a/tests/integration/fixtures/scheduler_string_test.yaml b/tests/integration/fixtures/scheduler_string_test.yaml
index 1188577e15..3dfe891370 100644
--- a/tests/integration/fixtures/scheduler_string_test.yaml
+++ b/tests/integration/fixtures/scheduler_string_test.yaml
@@ -75,20 +75,42 @@ script:
           App.scheduler.cancel_timeout(component1, "cancel_static_timeout");
           ESP_LOGI("test", "Cancelled static timeout using const char*");
 
+          // Test 6 & 7: Test defer with const char* overload using a test component
+          class TestDeferComponent : public Component {
+          public:
+            void test_static_defer() {
+              // Test 6: Static string literal with defer (const char* overload)
+              this->defer("static_defer_1", []() {
+                ESP_LOGI("test", "Static defer 1 fired");
+                id(timeout_counter) += 1;
+              });
+
+              // Test 7: Static const char* with defer
+              static const char* DEFER_NAME = "static_defer_2";
+              this->defer(DEFER_NAME, []() {
+                ESP_LOGI("test", "Static defer 2 fired");
+                id(timeout_counter) += 1;
+              });
+            }
+          };
+
+          static TestDeferComponent test_defer_component;
+          test_defer_component.test_static_defer();
+
   - id: test_dynamic_strings
     then:
       - logger.log: "Testing dynamic string timeouts and intervals"
       - lambda: |-
           auto *component2 = id(test_sensor2);
 
-          // Test 6: Dynamic string with set_timeout (std::string)
+          // Test 8: Dynamic string with set_timeout (std::string)
           std::string dynamic_name = "dynamic_timeout_" + std::to_string(id(dynamic_counter)++);
           App.scheduler.set_timeout(component2, dynamic_name, 100, []() {
             ESP_LOGI("test", "Dynamic timeout fired");
             id(timeout_counter) += 1;
           });
 
-          // Test 7: Dynamic string with set_interval
+          // Test 9: Dynamic string with set_interval
           std::string interval_name = "dynamic_interval_" + std::to_string(id(dynamic_counter)++);
           App.scheduler.set_interval(component2, interval_name, 250, [interval_name]() {
             ESP_LOGI("test", "Dynamic interval fired: %s", interval_name.c_str());
@@ -99,7 +121,7 @@ script:
             }
           });
 
-          // Test 8: Cancel with different string object but same content
+          // Test 10: Cancel with different string object but same content
           std::string cancel_name = "cancel_test";
           App.scheduler.set_timeout(component2, cancel_name, 2000, []() {
             ESP_LOGI("test", "This should be cancelled");
@@ -110,6 +132,21 @@ script:
           App.scheduler.cancel_timeout(component2, cancel_name_2);
           ESP_LOGI("test", "Cancelled timeout using different string object");
 
+          // Test 11: Dynamic string with defer (using std::string overload)
+          class TestDynamicDeferComponent : public Component {
+          public:
+            void test_dynamic_defer() {
+              std::string defer_name = "dynamic_defer_" + std::to_string(id(dynamic_counter)++);
+              this->defer(defer_name, [defer_name]() {
+                ESP_LOGI("test", "Dynamic defer fired: %s", defer_name.c_str());
+                id(timeout_counter) += 1;
+              });
+            }
+          };
+
+          static TestDynamicDeferComponent test_dynamic_defer_component;
+          test_dynamic_defer_component.test_dynamic_defer();
+
   - id: report_results
     then:
       - lambda: |-
diff --git a/tests/integration/test_scheduler_string_test.py b/tests/integration/test_scheduler_string_test.py
index b5ca07f9db..f3a36b2db7 100644
--- a/tests/integration/test_scheduler_string_test.py
+++ b/tests/integration/test_scheduler_string_test.py
@@ -26,8 +26,11 @@ async def test_scheduler_string_test(
     static_interval_cancelled = asyncio.Event()
     empty_string_timeout_fired = asyncio.Event()
     static_timeout_cancelled = asyncio.Event()
+    static_defer_1_fired = asyncio.Event()
+    static_defer_2_fired = asyncio.Event()
     dynamic_timeout_fired = asyncio.Event()
     dynamic_interval_fired = asyncio.Event()
+    dynamic_defer_fired = asyncio.Event()
     cancel_test_done = asyncio.Event()
     final_results_logged = asyncio.Event()
 
@@ -72,6 +75,15 @@ async def test_scheduler_string_test(
         elif "Cancelled static timeout using const char*" in clean_line:
             static_timeout_cancelled.set()
 
+        # Check for static defer tests
+        elif "Static defer 1 fired" in clean_line:
+            static_defer_1_fired.set()
+            timeout_count += 1
+
+        elif "Static defer 2 fired" in clean_line:
+            static_defer_2_fired.set()
+            timeout_count += 1
+
         # Check for dynamic string tests
         elif "Dynamic timeout fired" in clean_line:
             dynamic_timeout_fired.set()
@@ -81,6 +93,11 @@ async def test_scheduler_string_test(
             dynamic_interval_count += 1
             dynamic_interval_fired.set()
 
+        # Check for dynamic defer test
+        elif "Dynamic defer fired" in clean_line:
+            dynamic_defer_fired.set()
+            timeout_count += 1
+
         # Check for cancel test
         elif "Cancelled timeout using different string object" in clean_line:
             cancel_test_done.set()
@@ -133,6 +150,17 @@ async def test_scheduler_string_test(
             "Static timeout should have been cancelled"
         )
 
+        # Wait for static defer tests
+        try:
+            await asyncio.wait_for(static_defer_1_fired.wait(), timeout=0.5)
+        except asyncio.TimeoutError:
+            pytest.fail("Static defer 1 did not fire within 0.5 seconds")
+
+        try:
+            await asyncio.wait_for(static_defer_2_fired.wait(), timeout=0.5)
+        except asyncio.TimeoutError:
+            pytest.fail("Static defer 2 did not fire within 0.5 seconds")
+
         # Wait for dynamic string tests
         try:
             await asyncio.wait_for(dynamic_timeout_fired.wait(), timeout=1.0)
@@ -144,6 +172,12 @@ async def test_scheduler_string_test(
         except asyncio.TimeoutError:
             pytest.fail("Dynamic interval did not fire within 1.5 seconds")
 
+        # Wait for dynamic defer test
+        try:
+            await asyncio.wait_for(dynamic_defer_fired.wait(), timeout=1.0)
+        except asyncio.TimeoutError:
+            pytest.fail("Dynamic defer did not fire within 1 second")
+
         # Wait for cancel test
         try:
             await asyncio.wait_for(cancel_test_done.wait(), timeout=1.0)
@@ -157,7 +191,9 @@ async def test_scheduler_string_test(
             pytest.fail("Final results were not logged within 4 seconds")
 
         # Verify results
-        assert timeout_count >= 3, f"Expected at least 3 timeouts, got {timeout_count}"
+        assert timeout_count >= 6, (
+            f"Expected at least 6 timeouts (including defers), got {timeout_count}"
+        )
         assert interval_count >= 3, (
             f"Expected at least 3 interval fires, got {interval_count}"
         )

From 4e9e48e2e7c321d8ad3511681ab0439fde2d67b2 Mon Sep 17 00:00:00 2001
From: Thomas Rupprecht 
Date: Sat, 5 Jul 2025 08:23:24 +0200
Subject: [PATCH 233/293] [rtttl] trim extraneous whitespace in "ac_dimmer" in
 "PWM_BAD" list (#9318)

---
 esphome/components/rtttl/__init__.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py
index 0abd51a6f1..ebbe5366aa 100644
--- a/esphome/components/rtttl/__init__.py
+++ b/esphome/components/rtttl/__init__.py
@@ -57,14 +57,14 @@ def validate_parent_output_config(value):
     platform = value.get(CONF_PLATFORM)
     PWM_GOOD = ["esp8266_pwm", "ledc"]
     PWM_BAD = [
-        "ac_dimmer ",
+        "ac_dimmer",
         "esp32_dac",
-        "slow_pwm",
         "mcp4725",
-        "pca9685",
-        "tlc59208f",
         "my9231",
+        "pca9685",
+        "slow_pwm",
         "sm16716",
+        "tlc59208f",
     ]
 
     if platform in PWM_BAD:

From b0f8922056f985d9a01bdbadea0d40d41ff67d53 Mon Sep 17 00:00:00 2001
From: Adrian Freund 
Date: Sun, 6 Jul 2025 00:00:39 +0200
Subject: [PATCH 234/293] Mark ESPTime comparison operators as const (#9335)

---
 esphome/core/time.cpp | 10 +++++-----
 esphome/core/time.h   | 10 +++++-----
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp
index 672f5b98bf..f9652b5329 100644
--- a/esphome/core/time.cpp
+++ b/esphome/core/time.cpp
@@ -226,11 +226,11 @@ int32_t ESPTime::timezone_offset() {
   return offset;
 }
 
-bool ESPTime::operator<(ESPTime other) { return this->timestamp < other.timestamp; }
-bool ESPTime::operator<=(ESPTime other) { return this->timestamp <= other.timestamp; }
-bool ESPTime::operator==(ESPTime other) { return this->timestamp == other.timestamp; }
-bool ESPTime::operator>=(ESPTime other) { return this->timestamp >= other.timestamp; }
-bool ESPTime::operator>(ESPTime other) { return this->timestamp > other.timestamp; }
+bool ESPTime::operator<(const ESPTime &other) const { return this->timestamp < other.timestamp; }
+bool ESPTime::operator<=(const ESPTime &other) const { return this->timestamp <= other.timestamp; }
+bool ESPTime::operator==(const ESPTime &other) const { return this->timestamp == other.timestamp; }
+bool ESPTime::operator>=(const ESPTime &other) const { return this->timestamp >= other.timestamp; }
+bool ESPTime::operator>(const ESPTime &other) const { return this->timestamp > other.timestamp; }
 
 template bool increment_time_value(T ¤t, uint16_t begin, uint16_t end) {
   current++;
diff --git a/esphome/core/time.h b/esphome/core/time.h
index 5cbd9369fb..a53fca2346 100644
--- a/esphome/core/time.h
+++ b/esphome/core/time.h
@@ -109,10 +109,10 @@ struct ESPTime {
   void increment_second();
   /// Increment this clock instance by one day.
   void increment_day();
-  bool operator<(ESPTime other);
-  bool operator<=(ESPTime other);
-  bool operator==(ESPTime other);
-  bool operator>=(ESPTime other);
-  bool operator>(ESPTime other);
+  bool operator<(const ESPTime &other) const;
+  bool operator<=(const ESPTime &other) const;
+  bool operator==(const ESPTime &other) const;
+  bool operator>=(const ESPTime &other) const;
+  bool operator>(const ESPTime &other) const;
 };
 }  // namespace esphome

From a1291c27303fe7ad287a64b7faf427b9191119cd Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Sat, 5 Jul 2025 21:48:58 -0500
Subject: [PATCH 235/293] [ld2450] Reduce CPU usage, eliminate redundant sensor
 updates (#9334)

---
 esphome/components/ld2450/ld2450.cpp | 96 +++++++++++++++++++---------
 esphome/components/ld2450/ld2450.h   | 30 ++++++++-
 2 files changed, 96 insertions(+), 30 deletions(-)

diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp
index 0e1123db1a..4b87f1cea4 100644
--- a/esphome/components/ld2450/ld2450.cpp
+++ b/esphome/components/ld2450/ld2450.cpp
@@ -1,5 +1,6 @@
 #include "ld2450.h"
 #include 
+#include 
 #ifdef USE_NUMBER
 #include "esphome/components/number/number.h"
 #endif
@@ -123,16 +124,11 @@ static const uint8_t CMD_SET_ZONE = 0xC2;
 
 static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; };
 
-static inline std::string convert_signed_int_to_hex(int value) {
-  auto value_as_str = str_snprintf("%04x", 4, value & 0xFFFF);
-  return value_as_str;
-}
-
 static inline void convert_int_values_to_hex(const int *values, uint8_t *bytes) {
   for (int i = 0; i < 4; i++) {
-    std::string temp_hex = convert_signed_int_to_hex(values[i]);
-    bytes[i * 2] = std::stoi(temp_hex.substr(2, 2), nullptr, 16);      // Store high byte
-    bytes[i * 2 + 1] = std::stoi(temp_hex.substr(0, 2), nullptr, 16);  // Store low byte
+    uint16_t val = values[i] & 0xFFFF;
+    bytes[i * 2] = val & 0xFF;             // Store low byte first (little-endian)
+    bytes[i * 2 + 1] = (val >> 8) & 0xFF;  // Store high byte second
   }
 }
 
@@ -428,6 +424,12 @@ void LD2450Component::send_command_(uint8_t command, const uint8_t *command_valu
 //  [AA FF 03 00] [0E 03 B1 86 10 00 40 01] [00 00 00 00 00 00 00 00] [00 00 00 00 00 00 00 00] [55 CC]
 //   Header       Target 1                  Target 2                  Target 3                  End
 void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
+  // Early throttle check - moved before any processing to save CPU cycles
+  if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) {
+    ESP_LOGV(TAG, "Throttling: %d", this->throttle_);
+    return;
+  }
+
   if (len < 29) {  // header (4 bytes) + 8 x 3 target data + footer (2 bytes)
     ESP_LOGE(TAG, "Invalid message length");
     return;
@@ -441,11 +443,6 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
     return;
   }
 
-  if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) {
-    ESP_LOGV(TAG, "Throttling: %d", this->throttle_);
-    return;
-  }
-
   this->last_periodic_millis_ = App.get_loop_component_start_time();
 
   int16_t target_count = 0;
@@ -473,7 +470,10 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
     if (sx != nullptr) {
       val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]);
       tx = val;
-      sx->publish_state(val);
+      if (this->cached_target_data_[index].x != val) {
+        sx->publish_state(val);
+        this->cached_target_data_[index].x = val;
+      }
     }
     // Y
     start = TARGET_Y + index * 8;
@@ -481,14 +481,20 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
     if (sy != nullptr) {
       val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]);
       ty = val;
-      sy->publish_state(val);
+      if (this->cached_target_data_[index].y != val) {
+        sy->publish_state(val);
+        this->cached_target_data_[index].y = val;
+      }
     }
     // RESOLUTION
     start = TARGET_RESOLUTION + index * 8;
     sensor::Sensor *sr = this->move_resolution_sensors_[index];
     if (sr != nullptr) {
       val = (buffer[start + 1] << 8) | buffer[start];
-      sr->publish_state(val);
+      if (this->cached_target_data_[index].resolution != val) {
+        sr->publish_state(val);
+        this->cached_target_data_[index].resolution = val;
+      }
     }
 #endif
     // SPEED
@@ -502,13 +508,17 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
 #ifdef USE_SENSOR
     sensor::Sensor *ss = this->move_speed_sensors_[index];
     if (ss != nullptr) {
-      ss->publish_state(val);
+      if (this->cached_target_data_[index].speed != val) {
+        ss->publish_state(val);
+        this->cached_target_data_[index].speed = val;
+      }
     }
 #endif
     // DISTANCE
-    val = (uint16_t) sqrt(
-        pow(ld2450::decode_coordinate(buffer[TARGET_X + index * 8], buffer[(TARGET_X + index * 8) + 1]), 2) +
-        pow(ld2450::decode_coordinate(buffer[TARGET_Y + index * 8], buffer[(TARGET_Y + index * 8) + 1]), 2));
+    // Optimized: use already decoded tx and ty values, replace pow() with multiplication
+    int32_t x_squared = (int32_t) tx * tx;
+    int32_t y_squared = (int32_t) ty * ty;
+    val = (uint16_t) sqrt(x_squared + y_squared);
     td = val;
     if (val > 0) {
       target_count++;
@@ -516,7 +526,10 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
 #ifdef USE_SENSOR
     sensor::Sensor *sd = this->move_distance_sensors_[index];
     if (sd != nullptr) {
-      sd->publish_state(val);
+      if (this->cached_target_data_[index].distance != val) {
+        sd->publish_state(val);
+        this->cached_target_data_[index].distance = val;
+      }
     }
     // ANGLE
     angle = calculate_angle(static_cast(ty), static_cast(td));
@@ -525,7 +538,11 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
     }
     sensor::Sensor *sa = this->move_angle_sensors_[index];
     if (sa != nullptr) {
-      sa->publish_state(angle);
+      if (std::isnan(this->cached_target_data_[index].angle) ||
+          std::abs(this->cached_target_data_[index].angle - angle) > 0.1f) {
+        sa->publish_state(angle);
+        this->cached_target_data_[index].angle = angle;
+      }
     }
 #endif
 #ifdef USE_TEXT_SENSOR
@@ -536,7 +553,10 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
     }
     text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
     if (tsd != nullptr) {
-      tsd->publish_state(direction);
+      if (this->cached_target_data_[index].direction != direction) {
+        tsd->publish_state(direction);
+        this->cached_target_data_[index].direction = direction;
+      }
     }
 #endif
 
@@ -563,32 +583,50 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
     // Publish Still Target Count in Zones
     sensor::Sensor *szstc = this->zone_still_target_count_sensors_[index];
     if (szstc != nullptr) {
-      szstc->publish_state(zone_still_targets);
+      if (this->cached_zone_data_[index].still_count != zone_still_targets) {
+        szstc->publish_state(zone_still_targets);
+        this->cached_zone_data_[index].still_count = zone_still_targets;
+      }
     }
     // Publish Moving Target Count in Zones
     sensor::Sensor *szmtc = this->zone_moving_target_count_sensors_[index];
     if (szmtc != nullptr) {
-      szmtc->publish_state(zone_moving_targets);
+      if (this->cached_zone_data_[index].moving_count != zone_moving_targets) {
+        szmtc->publish_state(zone_moving_targets);
+        this->cached_zone_data_[index].moving_count = zone_moving_targets;
+      }
     }
     // Publish All Target Count in Zones
     sensor::Sensor *sztc = this->zone_target_count_sensors_[index];
     if (sztc != nullptr) {
-      sztc->publish_state(zone_all_targets);
+      if (this->cached_zone_data_[index].total_count != zone_all_targets) {
+        sztc->publish_state(zone_all_targets);
+        this->cached_zone_data_[index].total_count = zone_all_targets;
+      }
     }
 
   }  // End loop thru zones
 
   // Target Count
   if (this->target_count_sensor_ != nullptr) {
-    this->target_count_sensor_->publish_state(target_count);
+    if (this->cached_global_data_.target_count != target_count) {
+      this->target_count_sensor_->publish_state(target_count);
+      this->cached_global_data_.target_count = target_count;
+    }
   }
   // Still Target Count
   if (this->still_target_count_sensor_ != nullptr) {
-    this->still_target_count_sensor_->publish_state(still_target_count);
+    if (this->cached_global_data_.still_count != still_target_count) {
+      this->still_target_count_sensor_->publish_state(still_target_count);
+      this->cached_global_data_.still_count = still_target_count;
+    }
   }
   // Moving Target Count
   if (this->moving_target_count_sensor_ != nullptr) {
-    this->moving_target_count_sensor_->publish_state(moving_target_count);
+    if (this->cached_global_data_.moving_count != moving_target_count) {
+      this->moving_target_count_sensor_->publish_state(moving_target_count);
+      this->cached_global_data_.moving_count = moving_target_count;
+    }
   }
 #endif
 
diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h
index b0c19dc96c..5ddccab638 100644
--- a/esphome/components/ld2450/ld2450.h
+++ b/esphome/components/ld2450/ld2450.h
@@ -5,6 +5,8 @@
 #include "esphome/core/defines.h"
 #include "esphome/core/helpers.h"
 #include "esphome/core/preferences.h"
+#include 
+#include 
 #ifdef USE_SENSOR
 #include "esphome/components/sensor/sensor.h"
 #endif
@@ -100,7 +102,7 @@ class LD2450Component : public Component, public uart::UARTDevice {
   void dump_config() override;
   void loop() override;
   void set_presence_timeout();
-  void set_throttle(uint16_t value) { this->throttle_ = value; };
+  void set_throttle(uint16_t value) { this->throttle_ = value; }
   void read_all_info();
   void query_zone_info();
   void restart_and_read_all_info();
@@ -164,6 +166,32 @@ class LD2450Component : public Component, public uart::UARTDevice {
   Zone zone_config_[MAX_ZONES];
   std::string version_{};
   std::string mac_{};
+
+  // Change detection - cache previous values to avoid redundant publishes
+  // All values are initialized to sentinel values that are outside the valid sensor ranges
+  // to ensure the first real measurement is always published
+  struct CachedTargetData {
+    int16_t x = std::numeric_limits::min();             // -32768, outside range of -4860 to 4860
+    int16_t y = std::numeric_limits::min();             // -32768, outside range of 0 to 7560
+    int16_t speed = std::numeric_limits::min();         // -32768, outside practical sensor range
+    uint16_t resolution = std::numeric_limits::max();  // 65535, unlikely resolution value
+    uint16_t distance = std::numeric_limits::max();    // 65535, outside range of 0 to ~8990
+    float angle = NAN;                                           // NAN, safe sentinel for floats
+    std::string direction = "";                                  // Empty string, will differ from any real direction
+  } cached_target_data_[MAX_TARGETS];
+
+  struct CachedZoneData {
+    uint8_t still_count = std::numeric_limits::max();   // 255, unlikely zone count
+    uint8_t moving_count = std::numeric_limits::max();  // 255, unlikely zone count
+    uint8_t total_count = std::numeric_limits::max();   // 255, unlikely zone count
+  } cached_zone_data_[MAX_ZONES];
+
+  struct CachedGlobalData {
+    uint8_t target_count = std::numeric_limits::max();  // 255, max 3 targets possible
+    uint8_t still_count = std::numeric_limits::max();   // 255, max 3 targets possible
+    uint8_t moving_count = std::numeric_limits::max();  // 255, max 3 targets possible
+  } cached_global_data_;
+
 #ifdef USE_NUMBER
   ESPPreferenceObject pref_;  // only used when numbers are in use
   ZoneOfNumbers zone_numbers_[MAX_ZONES];

From f7019a4ed77f6dd4a703b43b38483225b9f1b0b2 Mon Sep 17 00:00:00 2001
From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com>
Date: Sun, 6 Jul 2025 04:56:53 +0200
Subject: [PATCH 236/293] [nextion] Memory optimization (#9338)

---
 esphome/components/nextion/display.py           |  1 +
 esphome/components/nextion/nextion.cpp          | 16 ++++++++++------
 esphome/components/nextion/nextion.h            | 16 ++++++++++------
 esphome/components/nextion/nextion_commands.cpp |  2 +-
 4 files changed, 22 insertions(+), 13 deletions(-)

diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py
index 0aa5efeba7..420f8f69c5 100644
--- a/esphome/components/nextion/display.py
+++ b/esphome/components/nextion/display.py
@@ -167,6 +167,7 @@ async def to_code(config):
         cg.add(var.set_wake_up_page(config[CONF_WAKE_UP_PAGE]))
 
     if CONF_START_UP_PAGE in config:
+        cg.add_define("USE_NEXTION_CONF_START_UP_PAGE")
         cg.add(var.set_start_up_page(config[CONF_START_UP_PAGE]))
 
     cg.add(var.set_auto_wake_on_touch(config[CONF_AUTO_WAKE_ON_TOUCH]))
diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp
index bb75385d8c..bcb1aced9a 100644
--- a/esphome/components/nextion/nextion.cpp
+++ b/esphome/components/nextion/nextion.cpp
@@ -167,13 +167,15 @@ void Nextion::dump_config() {
     ESP_LOGCONFIG(TAG, "  Touch Timeout:  %" PRIu16, this->touch_sleep_timeout_);
   }
 
-  if (this->wake_up_page_ != -1) {
-    ESP_LOGCONFIG(TAG, "  Wake Up Page:   %d", this->wake_up_page_);
+  if (this->wake_up_page_ != 255) {
+    ESP_LOGCONFIG(TAG, "  Wake Up Page:   %u", this->wake_up_page_);
   }
 
-  if (this->start_up_page_ != -1) {
-    ESP_LOGCONFIG(TAG, "  Start Up Page:  %d", this->start_up_page_);
+#ifdef USE_NEXTION_CONF_START_UP_PAGE
+  if (this->start_up_page_ != 255) {
+    ESP_LOGCONFIG(TAG, "  Start Up Page:  %u", this->start_up_page_);
   }
+#endif  // USE_NEXTION_CONF_START_UP_PAGE
 
 #ifdef USE_NEXTION_COMMAND_SPACING
   ESP_LOGCONFIG(TAG, "  Cmd spacing:      %u ms", this->command_pacer_.get_spacing());
@@ -301,12 +303,14 @@ void Nextion::loop() {
       this->set_backlight_brightness(this->brightness_.value());
     }
 
+#ifdef USE_NEXTION_CONF_START_UP_PAGE
     // Check if a startup page has been set and send the command
-    if (this->start_up_page_ >= 0) {
+    if (this->start_up_page_ != 255) {
       this->goto_page(this->start_up_page_);
     }
+#endif  // USE_NEXTION_CONF_START_UP_PAGE
 
-    if (this->wake_up_page_ >= 0) {
+    if (this->wake_up_page_ != 255) {
       this->set_wake_up_page(this->wake_up_page_);
     }
 
diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h
index 0b77d234f5..f5fa26b98c 100644
--- a/esphome/components/nextion/nextion.h
+++ b/esphome/components/nextion/nextion.h
@@ -1194,7 +1194,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
 
   /**
    * Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode.
-   * @param wake_up_page The page id, from 0 to the last page in Nextion. Set -1 (not set to any existing page) to
+   * @param wake_up_page The page id, from 0 to the last page in Nextion. Set 255 (not set to any existing page) to
    * wakes up to current page.
    *
    * Example:
@@ -1204,11 +1204,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
    *
    * The display will wake up to page 2.
    */
-  void set_wake_up_page(int16_t wake_up_page = -1);
+  void set_wake_up_page(uint8_t wake_up_page = 255);
 
+#ifdef USE_NEXTION_CONF_START_UP_PAGE
   /**
    * Sets which page Nextion loads when connecting to ESPHome.
-   * @param start_up_page The page id, from 0 to the last page in Nextion. Set -1 (not set to any existing page) to
+   * @param start_up_page The page id, from 0 to the last page in Nextion. Set 255 (not set to any existing page) to
    * wakes up to current page.
    *
    * Example:
@@ -1218,7 +1219,8 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
    *
    * The display will go to page 2 when it establishes a connection to ESPHome.
    */
-  void set_start_up_page(int16_t start_up_page = -1) { this->start_up_page_ = start_up_page; }
+  void set_start_up_page(uint8_t start_up_page = 255) { this->start_up_page_ = start_up_page; }
+#endif  // USE_NEXTION_CONF_START_UP_PAGE
 
   /**
    * Sets if Nextion should auto-wake from sleep when touch press occurs.
@@ -1344,8 +1346,10 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
   void process_serial_();
   bool is_updating_ = false;
   uint16_t touch_sleep_timeout_ = 0;
-  int16_t wake_up_page_ = -1;
-  int16_t start_up_page_ = -1;
+  uint8_t wake_up_page_ = 255;
+#ifdef USE_NEXTION_CONF_START_UP_PAGE
+  uint8_t start_up_page_ = 255;
+#endif  // USE_NEXTION_CONF_START_UP_PAGE
   bool auto_wake_on_touch_ = true;
   bool exit_reparse_on_start_ = false;
   bool skip_connection_handshake_ = false;
diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp
index 84aacd1868..f8307c6c4b 100644
--- a/esphome/components/nextion/nextion_commands.cpp
+++ b/esphome/components/nextion/nextion_commands.cpp
@@ -10,7 +10,7 @@ static const char *const TAG = "nextion";
 // Sleep safe commands
 void Nextion::soft_reset() { this->send_command_("rest"); }
 
-void Nextion::set_wake_up_page(int16_t wake_up_page) {
+void Nextion::set_wake_up_page(uint8_t wake_up_page) {
   this->wake_up_page_ = wake_up_page;
   this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true);
 }

From 20ba035e3b1dadc5d4a82a64a8c96e5fb86286f9 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Sat, 5 Jul 2025 22:30:18 -0500
Subject: [PATCH 237/293] Reduce RAM usage by optimizing Color constant storage
 (#9339)

---
 esphome/core/color.cpp |  8 +++-----
 esphome/core/color.h   | 32 ++++++++++++++++----------------
 2 files changed, 19 insertions(+), 21 deletions(-)

diff --git a/esphome/core/color.cpp b/esphome/core/color.cpp
index 58d995db2f..7e390b2354 100644
--- a/esphome/core/color.cpp
+++ b/esphome/core/color.cpp
@@ -2,10 +2,8 @@
 
 namespace esphome {
 
-const Color Color::BLACK(0, 0, 0, 0);
-const Color Color::WHITE(255, 255, 255, 255);
-
-const Color COLOR_BLACK(0, 0, 0, 0);
-const Color COLOR_WHITE(255, 255, 255, 255);
+// C++20 constinit ensures compile-time initialization (stored in ROM)
+constinit const Color Color::BLACK(0, 0, 0, 0);
+constinit const Color Color::WHITE(255, 255, 255, 255);
 
 }  // namespace esphome
diff --git a/esphome/core/color.h b/esphome/core/color.h
index 1c43fd9d3e..2b307bb438 100644
--- a/esphome/core/color.h
+++ b/esphome/core/color.h
@@ -5,7 +5,9 @@
 
 namespace esphome {
 
-inline static uint8_t esp_scale8(uint8_t i, uint8_t scale) { return (uint16_t(i) * (1 + uint16_t(scale))) / 256; }
+inline static constexpr uint8_t esp_scale8(uint8_t i, uint8_t scale) {
+  return (uint16_t(i) * (1 + uint16_t(scale))) / 256;
+}
 
 struct Color {
   union {
@@ -31,17 +33,20 @@ struct Color {
     uint32_t raw_32;
   };
 
-  inline Color() ESPHOME_ALWAYS_INLINE : r(0), g(0), b(0), w(0) {}  // NOLINT
-  inline Color(uint8_t red, uint8_t green, uint8_t blue) ESPHOME_ALWAYS_INLINE : r(red), g(green), b(blue), w(0) {}
+  inline constexpr Color() ESPHOME_ALWAYS_INLINE : raw_32(0) {}  // NOLINT
+  inline constexpr Color(uint8_t red, uint8_t green, uint8_t blue) ESPHOME_ALWAYS_INLINE : r(red),
+                                                                                           g(green),
+                                                                                           b(blue),
+                                                                                           w(0) {}
 
-  inline Color(uint8_t red, uint8_t green, uint8_t blue, uint8_t white) ESPHOME_ALWAYS_INLINE : r(red),
-                                                                                                g(green),
-                                                                                                b(blue),
-                                                                                                w(white) {}
-  inline explicit Color(uint32_t colorcode) ESPHOME_ALWAYS_INLINE : r((colorcode >> 16) & 0xFF),
-                                                                    g((colorcode >> 8) & 0xFF),
-                                                                    b((colorcode >> 0) & 0xFF),
-                                                                    w((colorcode >> 24) & 0xFF) {}
+  inline constexpr Color(uint8_t red, uint8_t green, uint8_t blue, uint8_t white) ESPHOME_ALWAYS_INLINE : r(red),
+                                                                                                          g(green),
+                                                                                                          b(blue),
+                                                                                                          w(white) {}
+  inline explicit constexpr Color(uint32_t colorcode) ESPHOME_ALWAYS_INLINE : r((colorcode >> 16) & 0xFF),
+                                                                              g((colorcode >> 8) & 0xFF),
+                                                                              b((colorcode >> 0) & 0xFF),
+                                                                              w((colorcode >> 24) & 0xFF) {}
 
   inline bool is_on() ESPHOME_ALWAYS_INLINE { return this->raw_32 != 0; }
 
@@ -169,9 +174,4 @@ struct Color {
   static const Color WHITE;
 };
 
-ESPDEPRECATED("Use Color::BLACK instead of COLOR_BLACK", "v1.21")
-extern const Color COLOR_BLACK;
-ESPDEPRECATED("Use Color::WHITE instead of COLOR_WHITE", "v1.21")
-extern const Color COLOR_WHITE;
-
 }  // namespace esphome

From 0bc18a82814ea7cfec35facd143e1316fd6414a1 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Sat, 5 Jul 2025 23:34:55 -0500
Subject: [PATCH 238/293] Eliminate API component guard variable to save 8
 bytes RAM (#9341)

---
 esphome/components/api/api_server.cpp |  8 ++++++++
 esphome/components/api/api_server.h   | 12 ++++++++++--
 2 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp
index 0fd9c1a228..4dc6fe2390 100644
--- a/esphome/components/api/api_server.cpp
+++ b/esphome/components/api/api_server.cpp
@@ -24,6 +24,14 @@ static const char *const TAG = "api";
 // APIServer
 APIServer *global_api_server = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
+#ifndef USE_API_YAML_SERVICES
+// Global empty vector to avoid guard variables (saves 8 bytes)
+// This is initialized at program startup before any threads
+static const std::vector empty_user_services{};
+
+const std::vector &get_empty_user_services_instance() { return empty_user_services; }
+#endif
+
 APIServer::APIServer() {
   global_api_server = this;
   // Pre-allocate shared write buffer
diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h
index 9dc2b4b7d6..f34fd55974 100644
--- a/esphome/components/api/api_server.h
+++ b/esphome/components/api/api_server.h
@@ -25,6 +25,11 @@ struct SavedNoisePsk {
 } PACKED;  // NOLINT
 #endif
 
+#ifndef USE_API_YAML_SERVICES
+// Forward declaration of helper function
+const std::vector &get_empty_user_services_instance();
+#endif
+
 class APIServer : public Component, public Controller {
  public:
   APIServer();
@@ -151,8 +156,11 @@ class APIServer : public Component, public Controller {
 #ifdef USE_API_YAML_SERVICES
     return this->user_services_;
 #else
-    static const std::vector EMPTY;
-    return this->user_services_ ? *this->user_services_ : EMPTY;
+    if (this->user_services_) {
+      return *this->user_services_;
+    }
+    // Return reference to global empty instance (no guard needed)
+    return get_empty_user_services_instance();
 #endif
   }
 

From 4673a5b48c523df1a7237efff988c9a133a1f4f2 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Sun, 6 Jul 2025 05:06:32 -0500
Subject: [PATCH 239/293] Eliminate web_server_idf guard variable to save 8
 bytes RAM (#9344)

---
 esphome/components/web_server_idf/web_server_idf.cpp | 9 +++++++++
 esphome/components/web_server_idf/web_server_idf.h   | 5 +----
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp
index 9478e4748c..d2447681f5 100644
--- a/esphome/components/web_server_idf/web_server_idf.cpp
+++ b/esphome/components/web_server_idf/web_server_idf.cpp
@@ -37,6 +37,15 @@ namespace web_server_idf {
 
 static const char *const TAG = "web_server_idf";
 
+// Global instance to avoid guard variable (saves 8 bytes)
+// This is initialized at program startup before any threads
+namespace {
+// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
+DefaultHeaders default_headers_instance;
+}  // namespace
+
+DefaultHeaders &DefaultHeaders::Instance() { return default_headers_instance; }
+
 void AsyncWebServer::end() {
   if (this->server_) {
     httpd_stop(this->server_);
diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h
index 8de25c8e96..e8e40ef9b0 100644
--- a/esphome/components/web_server_idf/web_server_idf.h
+++ b/esphome/components/web_server_idf/web_server_idf.h
@@ -328,10 +328,7 @@ class DefaultHeaders {
   void addHeader(const char *name, const char *value) { this->headers_.emplace_back(name, value); }
 
   // NOLINTNEXTLINE(readability-identifier-naming)
-  static DefaultHeaders &Instance() {
-    static DefaultHeaders instance;
-    return instance;
-  }
+  static DefaultHeaders &Instance();
 
  protected:
   std::vector> headers_;

From e061b6dc5529428e17c64a909112d21d79553bdb Mon Sep 17 00:00:00 2001
From: Keith Burzinski 
Date: Sun, 6 Jul 2025 08:37:50 -0500
Subject: [PATCH 240/293] [scd4x] Optimize logging + minor code clean-up
 (#9347)

---
 esphome/components/scd4x/scd4x.cpp | 108 ++++++++++++++++-------------
 esphome/components/scd4x/scd4x.h   |  21 +++---
 2 files changed, 71 insertions(+), 58 deletions(-)

diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp
index f617ffe276..4a700b70c2 100644
--- a/esphome/components/scd4x/scd4x.cpp
+++ b/esphome/components/scd4x/scd4x.cpp
@@ -7,6 +7,8 @@ namespace scd4x {
 
 static const char *const TAG = "scd4x";
 
+static const uint16_t SCD41_ID = 0x1408;
+static const uint16_t SCD40_ID = 0x440;
 static const uint16_t SCD4X_CMD_GET_SERIAL_NUMBER = 0x3682;
 static const uint16_t SCD4X_CMD_TEMPERATURE_OFFSET = 0x241d;
 static const uint16_t SCD4X_CMD_ALTITUDE_COMPENSATION = 0x2427;
@@ -23,8 +25,6 @@ static const uint16_t SCD4X_CMD_STOP_MEASUREMENTS = 0x3f86;
 static const uint16_t SCD4X_CMD_FACTORY_RESET = 0x3632;
 static const uint16_t SCD4X_CMD_GET_FEATURESET = 0x202f;
 static const float SCD4X_TEMPERATURE_OFFSET_MULTIPLIER = (1 << 16) / 175.0f;
-static const uint16_t SCD41_ID = 0x1408;
-static const uint16_t SCD40_ID = 0x440;
 
 void SCD4XComponent::setup() {
   ESP_LOGCONFIG(TAG, "Running setup");
@@ -51,47 +51,66 @@ void SCD4XComponent::setup() {
 
       if (!this->write_command(SCD4X_CMD_TEMPERATURE_OFFSET,
                                (uint16_t) (temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) {
-        ESP_LOGE(TAG, "Error setting temperature offset.");
+        ESP_LOGE(TAG, "Error setting temperature offset");
         this->error_code_ = MEASUREMENT_INIT_FAILED;
         this->mark_failed();
         return;
       }
 
-      // If pressure compensation available use it
-      // else use altitude
-      if (ambient_pressure_compensation_) {
-        if (!this->update_ambient_pressure_compensation_(ambient_pressure_)) {
-          ESP_LOGE(TAG, "Error setting ambient pressure compensation.");
+      // If pressure compensation available use it, else use altitude
+      if (this->ambient_pressure_compensation_) {
+        if (!this->update_ambient_pressure_compensation_(this->ambient_pressure_)) {
+          ESP_LOGE(TAG, "Error setting ambient pressure compensation");
           this->error_code_ = MEASUREMENT_INIT_FAILED;
           this->mark_failed();
           return;
         }
       } else {
-        if (!this->write_command(SCD4X_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) {
-          ESP_LOGE(TAG, "Error setting altitude compensation.");
+        if (!this->write_command(SCD4X_CMD_ALTITUDE_COMPENSATION, this->altitude_compensation_)) {
+          ESP_LOGE(TAG, "Error setting altitude compensation");
           this->error_code_ = MEASUREMENT_INIT_FAILED;
           this->mark_failed();
           return;
         }
       }
 
-      if (!this->write_command(SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) {
-        ESP_LOGE(TAG, "Error setting automatic self calibration.");
+      if (!this->write_command(SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION, this->enable_asc_ ? 1 : 0)) {
+        ESP_LOGE(TAG, "Error setting automatic self calibration");
         this->error_code_ = MEASUREMENT_INIT_FAILED;
         this->mark_failed();
         return;
       }
 
-      initialized_ = true;
+      this->initialized_ = true;
       // Finally start sensor measurements
       this->start_measurement_();
-      ESP_LOGD(TAG, "Sensor initialized");
     });
   });
 }
 
 void SCD4XComponent::dump_config() {
-  ESP_LOGCONFIG(TAG, "scd4x:");
+  static const char *const MM_PERIODIC_STR = "Periodic (5s)";
+  static const char *const MM_LOW_POWER_PERIODIC_STR = "Low power periodic (30s)";
+  static const char *const MM_SINGLE_SHOT_STR = "Single shot";
+  static const char *const MM_SINGLE_SHOT_RHT_ONLY_STR = "Single shot rht only";
+  const char *measurement_mode_str = MM_PERIODIC_STR;
+
+  switch (this->measurement_mode_) {
+    case PERIODIC:
+      // measurement_mode_str = MM_PERIODIC_STR;
+      break;
+    case LOW_POWER_PERIODIC:
+      measurement_mode_str = MM_LOW_POWER_PERIODIC_STR;
+      break;
+    case SINGLE_SHOT:
+      measurement_mode_str = MM_SINGLE_SHOT_STR;
+      break;
+    case SINGLE_SHOT_RHT_ONLY:
+      measurement_mode_str = MM_SINGLE_SHOT_RHT_ONLY_STR;
+      break;
+  }
+
+  ESP_LOGCONFIG(TAG, "SCD4X:");
   LOG_I2C_DEVICE(this);
   if (this->is_failed()) {
     switch (this->error_code_) {
@@ -102,16 +121,20 @@ void SCD4XComponent::dump_config() {
         ESP_LOGW(TAG, "Measurement Initialization failed");
         break;
       case SERIAL_NUMBER_IDENTIFICATION_FAILED:
-        ESP_LOGW(TAG, "Unable to read sensor firmware version");
+        ESP_LOGW(TAG, "Unable to read firmware version");
         break;
       default:
         ESP_LOGW(TAG, "Unknown setup error");
         break;
     }
   }
-  ESP_LOGCONFIG(TAG, "  Automatic self calibration: %s", ONOFF(this->enable_asc_));
+  ESP_LOGCONFIG(TAG,
+                "  Automatic self calibration: %s\n"
+                "  Measurement mode: %s\n"
+                "  Temperature offset: %.2f °C",
+                ONOFF(this->enable_asc_), measurement_mode_str, this->temperature_offset_);
   if (this->ambient_pressure_source_ != nullptr) {
-    ESP_LOGCONFIG(TAG, "  Dynamic ambient pressure compensation using sensor '%s'",
+    ESP_LOGCONFIG(TAG, "  Dynamic ambient pressure compensation using '%s'",
                   this->ambient_pressure_source_->get_name().c_str());
   } else {
     if (this->ambient_pressure_compensation_) {
@@ -126,21 +149,6 @@ void SCD4XComponent::dump_config() {
                     this->altitude_compensation_);
     }
   }
-  switch (this->measurement_mode_) {
-    case PERIODIC:
-      ESP_LOGCONFIG(TAG, "  Measurement mode: periodic (5s)");
-      break;
-    case LOW_POWER_PERIODIC:
-      ESP_LOGCONFIG(TAG, "  Measurement mode: low power periodic (30s)");
-      break;
-    case SINGLE_SHOT:
-      ESP_LOGCONFIG(TAG, "  Measurement mode: single shot");
-      break;
-    case SINGLE_SHOT_RHT_ONLY:
-      ESP_LOGCONFIG(TAG, "  Measurement mode: single shot rht only");
-      break;
-  }
-  ESP_LOGCONFIG(TAG, "  Temperature offset: %.2f °C", this->temperature_offset_);
   LOG_UPDATE_INTERVAL(this);
   LOG_SENSOR("  ", "CO2", this->co2_sensor_);
   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
@@ -148,20 +156,20 @@ void SCD4XComponent::dump_config() {
 }
 
 void SCD4XComponent::update() {
-  if (!initialized_) {
+  if (!this->initialized_) {
     return;
   }
 
   if (this->ambient_pressure_source_ != nullptr) {
     float pressure = this->ambient_pressure_source_->state;
     if (!std::isnan(pressure)) {
-      set_ambient_pressure_compensation(pressure);
+      this->set_ambient_pressure_compensation(pressure);
     }
   }
 
   uint32_t wait_time = 0;
   if (this->measurement_mode_ == SINGLE_SHOT || this->measurement_mode_ == SINGLE_SHOT_RHT_ONLY) {
-    start_measurement_();
+    this->start_measurement_();
     wait_time =
         this->measurement_mode_ == SINGLE_SHOT ? 5000 : 50;  // Single shot measurement takes 5 secs rht mode 50 ms
   }
@@ -176,12 +184,12 @@ void SCD4XComponent::update() {
 
     if (!this->read_data(raw_read_status) || raw_read_status == 0x00) {
       this->status_set_warning();
-      ESP_LOGW(TAG, "Data not ready yet!");
+      ESP_LOGW(TAG, "Data not ready");
       return;
     }
 
     if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) {
-      ESP_LOGW(TAG, "Error reading measurement!");
+      ESP_LOGW(TAG, "Error reading measurement");
       this->status_set_warning();
       return;  // NO RETRY
     }
@@ -218,7 +226,7 @@ bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentrati
   }
   this->set_timeout(500, [this, current_co2_concentration]() {
     if (this->write_command(SCD4X_CMD_PERFORM_FORCED_CALIBRATION, current_co2_concentration)) {
-      ESP_LOGD(TAG, "setting forced calibration Co2 level %d ppm", current_co2_concentration);
+      ESP_LOGD(TAG, "Setting forced calibration Co2 level %d ppm", current_co2_concentration);
       // frc takes 400 ms
       // because this method will be used very rarly
       // the simple approach with delay is ok
@@ -226,11 +234,11 @@ bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentrati
       if (!this->start_measurement_()) {
         return false;
       } else {
-        ESP_LOGD(TAG, "forced calibration complete");
+        ESP_LOGD(TAG, "Forced calibration complete");
       }
       return true;
     } else {
-      ESP_LOGE(TAG, "force calibration failed");
+      ESP_LOGE(TAG, "Force calibration failed");
       this->error_code_ = FRC_FAILED;
       this->status_set_warning();
       return false;
@@ -261,25 +269,25 @@ bool SCD4XComponent::factory_reset() {
 void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_hpa) {
   ambient_pressure_compensation_ = true;
   uint16_t new_ambient_pressure = (uint16_t) pressure_in_hpa;
-  if (!initialized_) {
-    ambient_pressure_ = new_ambient_pressure;
+  if (!this->initialized_) {
+    this->ambient_pressure_ = new_ambient_pressure;
     return;
   }
   // Only send pressure value if it has changed since last update
-  if (new_ambient_pressure != ambient_pressure_) {
-    update_ambient_pressure_compensation_(new_ambient_pressure);
-    ambient_pressure_ = new_ambient_pressure;
+  if (new_ambient_pressure != this->ambient_pressure_) {
+    this->update_ambient_pressure_compensation_(new_ambient_pressure);
+    this->ambient_pressure_ = new_ambient_pressure;
   } else {
-    ESP_LOGD(TAG, "ambient pressure compensation skipped - no change required");
+    ESP_LOGD(TAG, "Ambient pressure compensation skipped; no change required");
   }
 }
 
 bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_hpa) {
   if (this->write_command(SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION, pressure_in_hpa)) {
-    ESP_LOGD(TAG, "setting ambient pressure compensation to %d hPa", pressure_in_hpa);
+    ESP_LOGD(TAG, "Setting ambient pressure compensation to %d hPa", pressure_in_hpa);
     return true;
   } else {
-    ESP_LOGE(TAG, "Error setting ambient pressure compensation.");
+    ESP_LOGE(TAG, "Error setting ambient pressure compensation");
     return false;
   }
 }
@@ -304,7 +312,7 @@ bool SCD4XComponent::start_measurement_() {
   static uint8_t remaining_retries = 3;
   while (remaining_retries) {
     if (!this->write_command(measurement_command)) {
-      ESP_LOGE(TAG, "Error starting measurements.");
+      ESP_LOGE(TAG, "Error starting measurements");
       this->error_code_ = MEASUREMENT_INIT_FAILED;
       this->status_set_warning();
       if (--remaining_retries == 0)
diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h
index f2efb28ac1..237d226107 100644
--- a/esphome/components/scd4x/scd4x.h
+++ b/esphome/components/scd4x/scd4x.h
@@ -8,14 +8,20 @@
 namespace esphome {
 namespace scd4x {
 
-enum ERRORCODE {
+enum ErrorCode : uint8_t {
   COMMUNICATION_FAILED,
   SERIAL_NUMBER_IDENTIFICATION_FAILED,
   MEASUREMENT_INIT_FAILED,
   FRC_FAILED,
-  UNKNOWN
+  UNKNOWN,
+};
+
+enum MeasurementMode : uint8_t {
+  PERIODIC,
+  LOW_POWER_PERIODIC,
+  SINGLE_SHOT,
+  SINGLE_SHOT_RHT_ONLY,
 };
-enum MeasurementMode { PERIODIC, LOW_POWER_PERIODIC, SINGLE_SHOT, SINGLE_SHOT_RHT_ONLY };
 
 class SCD4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
  public:
@@ -39,15 +45,14 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri
  protected:
   bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa);
   bool start_measurement_();
-  ERRORCODE error_code_;
 
-  bool initialized_{false};
-
-  float temperature_offset_;
   uint16_t altitude_compensation_;
-  bool ambient_pressure_compensation_;
   uint16_t ambient_pressure_;
+  bool initialized_{false};
+  bool ambient_pressure_compensation_;
   bool enable_asc_;
+  float temperature_offset_;
+  ErrorCode error_code_;
   MeasurementMode measurement_mode_{PERIODIC};
   sensor::Sensor *co2_sensor_{nullptr};
   sensor::Sensor *temperature_sensor_{nullptr};

From e5a699a00443ab7eb8b722c0f1955dd2e58c9ddd Mon Sep 17 00:00:00 2001
From: Keith Burzinski 
Date: Sun, 6 Jul 2025 09:16:30 -0500
Subject: [PATCH 241/293] [ld2410] Reduce RAM usage, general clean-up (#9346)

---
 esphome/components/ld2410/button/__init__.py  |   6 +-
 .../ld2410/button/factory_reset_button.cpp    |   9 +
 ...{reset_button.h => factory_reset_button.h} |   4 +-
 .../components/ld2410/button/reset_button.cpp |   9 -
 esphome/components/ld2410/ld2410.cpp          | 548 ++++++++++--------
 esphome/components/ld2410/ld2410.h            |  87 +--
 6 files changed, 357 insertions(+), 306 deletions(-)
 create mode 100644 esphome/components/ld2410/button/factory_reset_button.cpp
 rename esphome/components/ld2410/button/{reset_button.h => factory_reset_button.h} (65%)
 delete mode 100644 esphome/components/ld2410/button/reset_button.cpp

diff --git a/esphome/components/ld2410/button/__init__.py b/esphome/components/ld2410/button/__init__.py
index 4cb50d707b..1cd56082c3 100644
--- a/esphome/components/ld2410/button/__init__.py
+++ b/esphome/components/ld2410/button/__init__.py
@@ -14,8 +14,8 @@ from esphome.const import (
 
 from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns
 
+FactoryResetButton = ld2410_ns.class_("FactoryResetButton", button.Button)
 QueryButton = ld2410_ns.class_("QueryButton", button.Button)
-ResetButton = ld2410_ns.class_("ResetButton", button.Button)
 RestartButton = ld2410_ns.class_("RestartButton", button.Button)
 
 CONF_QUERY_PARAMS = "query_params"
@@ -23,7 +23,7 @@ CONF_QUERY_PARAMS = "query_params"
 CONFIG_SCHEMA = {
     cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
     cv.Optional(CONF_FACTORY_RESET): button.button_schema(
-        ResetButton,
+        FactoryResetButton,
         device_class=DEVICE_CLASS_RESTART,
         entity_category=ENTITY_CATEGORY_CONFIG,
         icon=ICON_RESTART_ALERT,
@@ -47,7 +47,7 @@ async def to_code(config):
     if factory_reset_config := config.get(CONF_FACTORY_RESET):
         b = await button.new_button(factory_reset_config)
         await cg.register_parented(b, config[CONF_LD2410_ID])
-        cg.add(ld2410_component.set_reset_button(b))
+        cg.add(ld2410_component.set_factory_reset_button(b))
     if restart_config := config.get(CONF_RESTART):
         b = await button.new_button(restart_config)
         await cg.register_parented(b, config[CONF_LD2410_ID])
diff --git a/esphome/components/ld2410/button/factory_reset_button.cpp b/esphome/components/ld2410/button/factory_reset_button.cpp
new file mode 100644
index 0000000000..a848b02a9d
--- /dev/null
+++ b/esphome/components/ld2410/button/factory_reset_button.cpp
@@ -0,0 +1,9 @@
+#include "factory_reset_button.h"
+
+namespace esphome {
+namespace ld2410 {
+
+void FactoryResetButton::press_action() { this->parent_->factory_reset(); }
+
+}  // namespace ld2410
+}  // namespace esphome
diff --git a/esphome/components/ld2410/button/reset_button.h b/esphome/components/ld2410/button/factory_reset_button.h
similarity index 65%
rename from esphome/components/ld2410/button/reset_button.h
rename to esphome/components/ld2410/button/factory_reset_button.h
index 78dd92c9f5..45bf979033 100644
--- a/esphome/components/ld2410/button/reset_button.h
+++ b/esphome/components/ld2410/button/factory_reset_button.h
@@ -6,9 +6,9 @@
 namespace esphome {
 namespace ld2410 {
 
-class ResetButton : public button::Button, public Parented {
+class FactoryResetButton : public button::Button, public Parented {
  public:
-  ResetButton() = default;
+  FactoryResetButton() = default;
 
  protected:
   void press_action() override;
diff --git a/esphome/components/ld2410/button/reset_button.cpp b/esphome/components/ld2410/button/reset_button.cpp
deleted file mode 100644
index f16c5faa79..0000000000
--- a/esphome/components/ld2410/button/reset_button.cpp
+++ /dev/null
@@ -1,9 +0,0 @@
-#include "reset_button.h"
-
-namespace esphome {
-namespace ld2410 {
-
-void ResetButton::press_action() { this->parent_->factory_reset(); }
-
-}  // namespace ld2410
-}  // namespace esphome
diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp
index a34f99ee33..375d1088e8 100644
--- a/esphome/components/ld2410/ld2410.cpp
+++ b/esphome/components/ld2410/ld2410.cpp
@@ -18,11 +18,10 @@ namespace esphome {
 namespace ld2410 {
 
 static const char *const TAG = "ld2410";
-static const char *const NO_MAC = "08:05:04:03:02:01";
 static const char *const UNKNOWN_MAC = "unknown";
 static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
 
-enum BaudRateStructure : uint8_t {
+enum BaudRate : uint8_t {
   BAUD_RATE_9600 = 1,
   BAUD_RATE_19200 = 2,
   BAUD_RATE_38400 = 3,
@@ -33,23 +32,23 @@ enum BaudRateStructure : uint8_t {
   BAUD_RATE_460800 = 8,
 };
 
-enum DistanceResolutionStructure : uint8_t {
+enum DistanceResolution : uint8_t {
   DISTANCE_RESOLUTION_0_2 = 0x01,
   DISTANCE_RESOLUTION_0_75 = 0x00,
 };
 
-enum LightFunctionStructure : uint8_t {
+enum LightFunction : uint8_t {
   LIGHT_FUNCTION_OFF = 0x00,
   LIGHT_FUNCTION_BELOW = 0x01,
   LIGHT_FUNCTION_ABOVE = 0x02,
 };
 
-enum OutPinLevelStructure : uint8_t {
+enum OutPinLevel : uint8_t {
   OUT_PIN_LEVEL_LOW = 0x00,
   OUT_PIN_LEVEL_HIGH = 0x01,
 };
 
-enum PeriodicDataStructure : uint8_t {
+enum PeriodicData : uint8_t {
   DATA_TYPES = 6,
   TARGET_STATES = 8,
   MOVING_TARGET_LOW = 9,
@@ -67,12 +66,12 @@ enum PeriodicDataStructure : uint8_t {
 };
 
 enum PeriodicDataValue : uint8_t {
-  HEAD = 0xAA,
-  END = 0x55,
+  HEADER = 0xAA,
+  FOOTER = 0x55,
   CHECK = 0x00,
 };
 
-enum AckDataStructure : uint8_t {
+enum AckData : uint8_t {
   COMMAND = 6,
   COMMAND_STATUS = 7,
 };
@@ -80,11 +79,11 @@ enum AckDataStructure : uint8_t {
 // Memory-efficient lookup tables
 struct StringToUint8 {
   const char *str;
-  uint8_t value;
+  const uint8_t value;
 };
 
 struct Uint8ToString {
-  uint8_t value;
+  const uint8_t value;
   const char *str;
 };
 
@@ -144,96 +143,119 @@ template const char *find_str(const Uint8ToString (&arr)[N], uint8_t v
 }
 
 // Commands
-static const uint8_t CMD_ENABLE_CONF = 0xFF;
-static const uint8_t CMD_DISABLE_CONF = 0xFE;
-static const uint8_t CMD_ENABLE_ENG = 0x62;
-static const uint8_t CMD_DISABLE_ENG = 0x63;
-static const uint8_t CMD_MAXDIST_DURATION = 0x60;
-static const uint8_t CMD_QUERY = 0x61;
-static const uint8_t CMD_GATE_SENS = 0x64;
-static const uint8_t CMD_VERSION = 0xA0;
-static const uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0xAB;
-static const uint8_t CMD_SET_DISTANCE_RESOLUTION = 0xAA;
-static const uint8_t CMD_QUERY_LIGHT_CONTROL = 0xAE;
-static const uint8_t CMD_SET_LIGHT_CONTROL = 0xAD;
-static const uint8_t CMD_SET_BAUD_RATE = 0xA1;
-static const uint8_t CMD_BT_PASSWORD = 0xA9;
-static const uint8_t CMD_MAC = 0xA5;
-static const uint8_t CMD_RESET = 0xA2;
-static const uint8_t CMD_RESTART = 0xA3;
-static const uint8_t CMD_BLUETOOTH = 0xA4;
+static constexpr uint8_t CMD_ENABLE_CONF = 0xFF;
+static constexpr uint8_t CMD_DISABLE_CONF = 0xFE;
+static constexpr uint8_t CMD_ENABLE_ENG = 0x62;
+static constexpr uint8_t CMD_DISABLE_ENG = 0x63;
+static constexpr uint8_t CMD_MAXDIST_DURATION = 0x60;
+static constexpr uint8_t CMD_QUERY = 0x61;
+static constexpr uint8_t CMD_GATE_SENS = 0x64;
+static constexpr uint8_t CMD_QUERY_VERSION = 0xA0;
+static constexpr uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0xAB;
+static constexpr uint8_t CMD_SET_DISTANCE_RESOLUTION = 0xAA;
+static constexpr uint8_t CMD_QUERY_LIGHT_CONTROL = 0xAE;
+static constexpr uint8_t CMD_SET_LIGHT_CONTROL = 0xAD;
+static constexpr uint8_t CMD_SET_BAUD_RATE = 0xA1;
+static constexpr uint8_t CMD_BT_PASSWORD = 0xA9;
+static constexpr uint8_t CMD_QUERY_MAC_ADDRESS = 0xA5;
+static constexpr uint8_t CMD_RESET = 0xA2;
+static constexpr uint8_t CMD_RESTART = 0xA3;
+static constexpr uint8_t CMD_BLUETOOTH = 0xA4;
 // Commands values
-static const uint8_t CMD_MAX_MOVE_VALUE = 0x00;
-static const uint8_t CMD_MAX_STILL_VALUE = 0x01;
-static const uint8_t CMD_DURATION_VALUE = 0x02;
+static constexpr uint8_t CMD_MAX_MOVE_VALUE = 0x00;
+static constexpr uint8_t CMD_MAX_STILL_VALUE = 0x01;
+static constexpr uint8_t CMD_DURATION_VALUE = 0x02;
+// Header & Footer size
+static constexpr uint8_t HEADER_FOOTER_SIZE = 4;
 // Command Header & Footer
-static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA};
-static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01};
+static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA};
+static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01};
 // Data Header & Footer
-static const uint8_t DATA_FRAME_HEADER[4] = {0xF4, 0xF3, 0xF2, 0xF1};
-static const uint8_t DATA_FRAME_END[4] = {0xF8, 0xF7, 0xF6, 0xF5};
+static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xF4, 0xF3, 0xF2, 0xF1};
+static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0xF6, 0xF5};
+// MAC address the module uses when Bluetooth is disabled
+static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
 
 static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; }
 
+static bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
+  for (uint8_t i = 0; i < HEADER_FOOTER_SIZE; i++) {
+    if (header_footer[i] != buffer[i]) {
+      return false;  // Mismatch in header/footer
+    }
+  }
+  return true;  // Valid header/footer
+}
+
 void LD2410Component::dump_config() {
-  ESP_LOGCONFIG(TAG, "LD2410:");
+  std::string mac_str =
+      mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
+  std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
+                                    this->version_[4], this->version_[3], this->version_[2]);
+  ESP_LOGCONFIG(TAG,
+                "LD2410:\n"
+                "  Firmware version: %s\n"
+                "  MAC address: %s\n"
+                "  Throttle: %u ms",
+                version.c_str(), mac_str.c_str(), this->throttle_);
 #ifdef USE_BINARY_SENSOR
-  LOG_BINARY_SENSOR("  ", "TargetBinarySensor", this->target_binary_sensor_);
-  LOG_BINARY_SENSOR("  ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_);
-  LOG_BINARY_SENSOR("  ", "StillTargetBinarySensor", this->still_target_binary_sensor_);
-  LOG_BINARY_SENSOR("  ", "OutPinPresenceStatusBinarySensor", this->out_pin_presence_status_binary_sensor_);
-#endif
-#ifdef USE_SWITCH
-  LOG_SWITCH("  ", "EngineeringModeSwitch", this->engineering_mode_switch_);
-  LOG_SWITCH("  ", "BluetoothSwitch", this->bluetooth_switch_);
-#endif
-#ifdef USE_BUTTON
-  LOG_BUTTON("  ", "ResetButton", this->reset_button_);
-  LOG_BUTTON("  ", "RestartButton", this->restart_button_);
-  LOG_BUTTON("  ", "QueryButton", this->query_button_);
+  ESP_LOGCONFIG(TAG, "Binary Sensors:");
+  LOG_BINARY_SENSOR("  ", "Target", this->target_binary_sensor_);
+  LOG_BINARY_SENSOR("  ", "MovingTarget", this->moving_target_binary_sensor_);
+  LOG_BINARY_SENSOR("  ", "StillTarget", this->still_target_binary_sensor_);
+  LOG_BINARY_SENSOR("  ", "OutPinPresenceStatus", this->out_pin_presence_status_binary_sensor_);
 #endif
 #ifdef USE_SENSOR
-  LOG_SENSOR("  ", "LightSensor", this->light_sensor_);
-  LOG_SENSOR("  ", "MovingTargetDistanceSensor", this->moving_target_distance_sensor_);
-  LOG_SENSOR("  ", "StillTargetDistanceSensor", this->still_target_distance_sensor_);
-  LOG_SENSOR("  ", "MovingTargetEnergySensor", this->moving_target_energy_sensor_);
-  LOG_SENSOR("  ", "StillTargetEnergySensor", this->still_target_energy_sensor_);
-  LOG_SENSOR("  ", "DetectionDistanceSensor", this->detection_distance_sensor_);
-  for (sensor::Sensor *s : this->gate_still_sensors_) {
-    LOG_SENSOR("  ", "NthGateStillSesnsor", s);
-  }
+  ESP_LOGCONFIG(TAG, "Sensors:");
+  LOG_SENSOR("  ", "Light", this->light_sensor_);
+  LOG_SENSOR("  ", "DetectionDistance", this->detection_distance_sensor_);
+  LOG_SENSOR("  ", "MovingTargetDistance", this->moving_target_distance_sensor_);
+  LOG_SENSOR("  ", "MovingTargetEnergy", this->moving_target_energy_sensor_);
+  LOG_SENSOR("  ", "StillTargetDistance", this->still_target_distance_sensor_);
+  LOG_SENSOR("  ", "StillTargetEnergy", this->still_target_energy_sensor_);
   for (sensor::Sensor *s : this->gate_move_sensors_) {
-    LOG_SENSOR("  ", "NthGateMoveSesnsor", s);
+    LOG_SENSOR("  ", "GateMove", s);
+  }
+  for (sensor::Sensor *s : this->gate_still_sensors_) {
+    LOG_SENSOR("  ", "GateStill", s);
   }
 #endif
 #ifdef USE_TEXT_SENSOR
-  LOG_TEXT_SENSOR("  ", "VersionTextSensor", this->version_text_sensor_);
-  LOG_TEXT_SENSOR("  ", "MacTextSensor", this->mac_text_sensor_);
-#endif
-#ifdef USE_SELECT
-  LOG_SELECT("  ", "LightFunctionSelect", this->light_function_select_);
-  LOG_SELECT("  ", "OutPinLevelSelect", this->out_pin_level_select_);
-  LOG_SELECT("  ", "DistanceResolutionSelect", this->distance_resolution_select_);
-  LOG_SELECT("  ", "BaudRateSelect", this->baud_rate_select_);
+  ESP_LOGCONFIG(TAG, "Text Sensors:");
+  LOG_TEXT_SENSOR("  ", "Mac", this->mac_text_sensor_);
+  LOG_TEXT_SENSOR("  ", "Version", this->version_text_sensor_);
 #endif
 #ifdef USE_NUMBER
-  LOG_NUMBER("  ", "LightThresholdNumber", this->light_threshold_number_);
-  LOG_NUMBER("  ", "MaxStillDistanceGateNumber", this->max_still_distance_gate_number_);
-  LOG_NUMBER("  ", "MaxMoveDistanceGateNumber", this->max_move_distance_gate_number_);
-  LOG_NUMBER("  ", "TimeoutNumber", this->timeout_number_);
-  for (number::Number *n : this->gate_still_threshold_numbers_) {
-    LOG_NUMBER("  ", "Still Thresholds Number", n);
-  }
+  ESP_LOGCONFIG(TAG, "Numbers:");
+  LOG_NUMBER("  ", "LightThreshold", this->light_threshold_number_);
+  LOG_NUMBER("  ", "MaxMoveDistanceGate", this->max_move_distance_gate_number_);
+  LOG_NUMBER("  ", "MaxStillDistanceGate", this->max_still_distance_gate_number_);
+  LOG_NUMBER("  ", "Timeout", this->timeout_number_);
   for (number::Number *n : this->gate_move_threshold_numbers_) {
-    LOG_NUMBER("  ", "Move Thresholds Number", n);
+    LOG_NUMBER("  ", "MoveThreshold", n);
+  }
+  for (number::Number *n : this->gate_still_threshold_numbers_) {
+    LOG_NUMBER("  ", "StillThreshold", n);
   }
 #endif
-  this->read_all_info();
-  ESP_LOGCONFIG(TAG,
-                "  Throttle: %ums\n"
-                "  MAC address: %s\n"
-                "  Firmware version: %s",
-                this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str());
+#ifdef USE_SELECT
+  ESP_LOGCONFIG(TAG, "Selects:");
+  LOG_SELECT("  ", "BaudRate", this->baud_rate_select_);
+  LOG_SELECT("  ", "DistanceResolution", this->distance_resolution_select_);
+  LOG_SELECT("  ", "LightFunction", this->light_function_select_);
+  LOG_SELECT("  ", "OutPinLevel", this->out_pin_level_select_);
+#endif
+#ifdef USE_SWITCH
+  ESP_LOGCONFIG(TAG, "Switches:");
+  LOG_SWITCH("  ", "Bluetooth", this->bluetooth_switch_);
+  LOG_SWITCH("  ", "EngineeringMode", this->engineering_mode_switch_);
+#endif
+#ifdef USE_BUTTON
+  ESP_LOGCONFIG(TAG, "Buttons:");
+  LOG_BUTTON("  ", "FactoryReset", this->factory_reset_button_);
+  LOG_BUTTON("  ", "Query", this->query_button_);
+  LOG_BUTTON("  ", "Restart", this->restart_button_);
+#endif
 }
 
 void LD2410Component::setup() {
@@ -246,12 +268,12 @@ void LD2410Component::read_all_info() {
   this->get_version_();
   this->get_mac_();
   this->get_distance_resolution_();
-  this->get_light_control_();
+  this->query_light_control_();
   this->query_parameters_();
   this->set_config_mode_(false);
 #ifdef USE_SELECT
   const auto baud_rate = std::to_string(this->parent_->get_baud_rate());
-  if (this->baud_rate_select_ != nullptr && this->baud_rate_select_->state != baud_rate) {
+  if (this->baud_rate_select_ != nullptr) {
     this->baud_rate_select_->publish_state(baud_rate);
   }
 #endif
@@ -264,66 +286,59 @@ void LD2410Component::restart_and_read_all_info() {
 }
 
 void LD2410Component::loop() {
-  const int max_line_length = 80;
-  static uint8_t buffer[max_line_length];
-
-  while (available()) {
-    this->readline_(read(), buffer, max_line_length);
+  while (this->available()) {
+    this->readline_(this->read());
   }
 }
 
-void LD2410Component::send_command_(uint8_t command, const uint8_t *command_value, int command_value_len) {
+void LD2410Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
   ESP_LOGV(TAG, "Sending COMMAND %02X", command);
-  // frame start bytes
-  this->write_array(CMD_FRAME_HEADER, 4);
+  // frame header bytes
+  this->write_array(CMD_FRAME_HEADER, sizeof(CMD_FRAME_HEADER));
   // length bytes
-  int len = 2;
-  if (command_value != nullptr)
+  uint8_t len = 2;
+  if (command_value != nullptr) {
     len += command_value_len;
-  this->write_byte(lowbyte(len));
-  this->write_byte(highbyte(len));
-
-  // command
-  this->write_byte(lowbyte(command));
-  this->write_byte(highbyte(command));
+  }
+  uint8_t len_cmd[] = {lowbyte(len), highbyte(len), command, 0x00};
+  this->write_array(len_cmd, sizeof(len_cmd));
 
   // command value bytes
   if (command_value != nullptr) {
-    for (int i = 0; i < command_value_len; i++) {
+    for (uint8_t i = 0; i < command_value_len; i++) {
       this->write_byte(command_value[i]);
     }
   }
-  // frame end bytes
-  this->write_array(CMD_FRAME_END, 4);
+  // frame footer bytes
+  this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER));
   // FIXME to remove
   delay(50);  // NOLINT
 }
 
-void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) {
-  if (len < 12)
-    return;  // 4 frame start bytes + 2 length bytes + 1 data end byte + 1 crc byte + 4 frame end bytes
-  if (buffer[0] != 0xF4 || buffer[1] != 0xF3 || buffer[2] != 0xF2 || buffer[3] != 0xF1)  // check 4 frame start bytes
+void LD2410Component::handle_periodic_data_() {
+  // Reduce data update rate to reduce home assistant database growth
+  // Check this first to prevent unnecessary processing done in later checks/parsing
+  if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) {
     return;
-  if (buffer[7] != HEAD || buffer[len - 6] != END || buffer[len - 5] != CHECK)  // Check constant values
-    return;  // data head=0xAA, data end=0x55, crc=0x00
-
-  /*
-    Reduce data update rate to prevent home assistant database size grow fast
-  */
-  int32_t current_millis = App.get_loop_component_start_time();
-  if (current_millis - last_periodic_millis_ < this->throttle_)
+  }
+  // 4 frame header bytes + 2 length bytes + 1 data end byte + 1 crc byte + 4 frame footer bytes
+  // data header=0xAA, data footer=0x55, crc=0x00
+  if (this->buffer_pos_ < 12 || !ld2410::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) ||
+      this->buffer_data_[7] != HEADER || this->buffer_data_[this->buffer_pos_ - 6] != FOOTER ||
+      this->buffer_data_[this->buffer_pos_ - 5] != CHECK) {
     return;
-  last_periodic_millis_ = current_millis;
+  }
+  // Save the timestamp after validating the frame so, if invalid, we'll take the next frame immediately
+  this->last_periodic_millis_ = App.get_loop_component_start_time();
 
   /*
     Data Type: 7th
     0x01: Engineering mode
     0x02: Normal mode
   */
-  bool engineering_mode = buffer[DATA_TYPES] == 0x01;
+  bool engineering_mode = this->buffer_data_[DATA_TYPES] == 0x01;
 #ifdef USE_SWITCH
-  if (this->engineering_mode_switch_ != nullptr &&
-      current_millis - last_engineering_mode_change_millis_ > this->throttle_) {
+  if (this->engineering_mode_switch_ != nullptr) {
     this->engineering_mode_switch_->publish_state(engineering_mode);
   }
 #endif
@@ -335,7 +350,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) {
     0x02 = Still targets
     0x03 = Moving+Still targets
   */
-  char target_state = buffer[TARGET_STATES];
+  char target_state = this->buffer_data_[TARGET_STATES];
   if (this->target_binary_sensor_ != nullptr) {
     this->target_binary_sensor_->publish_state(target_state != 0x00);
   }
@@ -355,27 +370,30 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) {
   */
 #ifdef USE_SENSOR
   if (this->moving_target_distance_sensor_ != nullptr) {
-    int new_moving_target_distance = ld2410::two_byte_to_int(buffer[MOVING_TARGET_LOW], buffer[MOVING_TARGET_HIGH]);
+    int new_moving_target_distance =
+        ld2410::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]);
     if (this->moving_target_distance_sensor_->get_state() != new_moving_target_distance)
       this->moving_target_distance_sensor_->publish_state(new_moving_target_distance);
   }
   if (this->moving_target_energy_sensor_ != nullptr) {
-    int new_moving_target_energy = buffer[MOVING_ENERGY];
+    int new_moving_target_energy = this->buffer_data_[MOVING_ENERGY];
     if (this->moving_target_energy_sensor_->get_state() != new_moving_target_energy)
       this->moving_target_energy_sensor_->publish_state(new_moving_target_energy);
   }
   if (this->still_target_distance_sensor_ != nullptr) {
-    int new_still_target_distance = ld2410::two_byte_to_int(buffer[STILL_TARGET_LOW], buffer[STILL_TARGET_HIGH]);
+    int new_still_target_distance =
+        ld2410::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]);
     if (this->still_target_distance_sensor_->get_state() != new_still_target_distance)
       this->still_target_distance_sensor_->publish_state(new_still_target_distance);
   }
   if (this->still_target_energy_sensor_ != nullptr) {
-    int new_still_target_energy = buffer[STILL_ENERGY];
+    int new_still_target_energy = this->buffer_data_[STILL_ENERGY];
     if (this->still_target_energy_sensor_->get_state() != new_still_target_energy)
       this->still_target_energy_sensor_->publish_state(new_still_target_energy);
   }
   if (this->detection_distance_sensor_ != nullptr) {
-    int new_detect_distance = ld2410::two_byte_to_int(buffer[DETECT_DISTANCE_LOW], buffer[DETECT_DISTANCE_HIGH]);
+    int new_detect_distance =
+        ld2410::two_byte_to_int(this->buffer_data_[DETECT_DISTANCE_LOW], this->buffer_data_[DETECT_DISTANCE_HIGH]);
     if (this->detection_distance_sensor_->get_state() != new_detect_distance)
       this->detection_distance_sensor_->publish_state(new_detect_distance);
   }
@@ -388,7 +406,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) {
     for (std::vector::size_type i = 0; i != this->gate_move_sensors_.size(); i++) {
       sensor::Sensor *s = this->gate_move_sensors_[i];
       if (s != nullptr) {
-        s->publish_state(buffer[MOVING_SENSOR_START + i]);
+        s->publish_state(this->buffer_data_[MOVING_SENSOR_START + i]);
       }
     }
     /*
@@ -397,16 +415,17 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) {
     for (std::vector::size_type i = 0; i != this->gate_still_sensors_.size(); i++) {
       sensor::Sensor *s = this->gate_still_sensors_[i];
       if (s != nullptr) {
-        s->publish_state(buffer[STILL_SENSOR_START + i]);
+        s->publish_state(this->buffer_data_[STILL_SENSOR_START + i]);
       }
     }
     /*
       Light sensor: 38th bytes
     */
     if (this->light_sensor_ != nullptr) {
-      int new_light_sensor = buffer[LIGHT_SENSOR];
-      if (this->light_sensor_->get_state() != new_light_sensor)
+      int new_light_sensor = this->buffer_data_[LIGHT_SENSOR];
+      if (this->light_sensor_->get_state() != new_light_sensor) {
         this->light_sensor_->publish_state(new_light_sensor);
+      }
     }
   } else {
     for (auto *s : this->gate_move_sensors_) {
@@ -427,7 +446,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) {
 #ifdef USE_BINARY_SENSOR
   if (engineering_mode) {
     if (this->out_pin_presence_status_binary_sensor_ != nullptr) {
-      this->out_pin_presence_status_binary_sensor_->publish_state(buffer[OUT_PIN_SENSOR] == 0x01);
+      this->out_pin_presence_status_binary_sensor_->publish_state(this->buffer_data_[OUT_PIN_SENSOR] == 0x01);
     }
   } else {
     if (this->out_pin_presence_status_binary_sensor_ != nullptr) {
@@ -439,127 +458,149 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) {
 
 #ifdef USE_NUMBER
 std::function set_number_value(number::Number *n, float value) {
-  float normalized_value = value * 1.0;
-  if (n != nullptr && (!n->has_state() || n->state != normalized_value)) {
-    n->state = normalized_value;
-    return [n, normalized_value]() { n->publish_state(normalized_value); };
+  if (n != nullptr && (!n->has_state() || n->state != value)) {
+    n->state = value;
+    return [n, value]() { n->publish_state(value); };
   }
   return []() {};
 }
 #endif
 
-bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
-  ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", buffer[COMMAND]);
-  if (len < 10) {
+bool LD2410Component::handle_ack_data_() {
+  ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]);
+  if (this->buffer_pos_ < 10) {
     ESP_LOGE(TAG, "Invalid length");
     return true;
   }
-  if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) {  // check 4 frame start bytes
-    ESP_LOGE(TAG, "Invalid header");
+  if (!ld2410::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) {
+    ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str());
     return true;
   }
-  if (buffer[COMMAND_STATUS] != 0x01) {
+  if (this->buffer_data_[COMMAND_STATUS] != 0x01) {
     ESP_LOGE(TAG, "Invalid status");
     return true;
   }
-  if (ld2410::two_byte_to_int(buffer[8], buffer[9]) != 0x00) {
-    ESP_LOGE(TAG, "Invalid command: %u, %u", buffer[8], buffer[9]);
+  if (ld2410::two_byte_to_int(this->buffer_data_[8], this->buffer_data_[9]) != 0x00) {
+    ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]);
     return true;
   }
 
-  switch (buffer[COMMAND]) {
-    case lowbyte(CMD_ENABLE_CONF):
+  switch (this->buffer_data_[COMMAND]) {
+    case CMD_ENABLE_CONF:
       ESP_LOGV(TAG, "Enable conf");
       break;
-    case lowbyte(CMD_DISABLE_CONF):
+
+    case CMD_DISABLE_CONF:
       ESP_LOGV(TAG, "Disabled conf");
       break;
-    case lowbyte(CMD_SET_BAUD_RATE):
+
+    case CMD_SET_BAUD_RATE:
       ESP_LOGV(TAG, "Baud rate change");
 #ifdef USE_SELECT
       if (this->baud_rate_select_ != nullptr) {
-        ESP_LOGE(TAG, "Configure baud rate to %s and reinstall", this->baud_rate_select_->state.c_str());
+        ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str());
       }
 #endif
       break;
-    case lowbyte(CMD_VERSION):
-      this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]);
-      ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str());
+
+    case CMD_QUERY_VERSION: {
+      std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
+      std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
+                                        this->version_[4], this->version_[3], this->version_[2]);
+      ESP_LOGV(TAG, "Firmware version: %s", version.c_str());
 #ifdef USE_TEXT_SENSOR
       if (this->version_text_sensor_ != nullptr) {
-        this->version_text_sensor_->publish_state(this->version_);
+        this->version_text_sensor_->publish_state(version);
       }
 #endif
       break;
-    case lowbyte(CMD_QUERY_DISTANCE_RESOLUTION): {
-      std::string distance_resolution =
-          find_str(DISTANCE_RESOLUTIONS_BY_UINT, ld2410::two_byte_to_int(buffer[10], buffer[11]));
-      ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution.c_str());
+    }
+
+    case CMD_QUERY_DISTANCE_RESOLUTION: {
+      const auto *distance_resolution = find_str(DISTANCE_RESOLUTIONS_BY_UINT, this->buffer_data_[10]);
+      ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution);
 #ifdef USE_SELECT
-      if (this->distance_resolution_select_ != nullptr &&
-          this->distance_resolution_select_->state != distance_resolution) {
+      if (this->distance_resolution_select_ != nullptr) {
         this->distance_resolution_select_->publish_state(distance_resolution);
       }
 #endif
-    } break;
-    case lowbyte(CMD_QUERY_LIGHT_CONTROL): {
-      this->light_function_ = find_str(LIGHT_FUNCTIONS_BY_UINT, buffer[10]);
-      this->light_threshold_ = buffer[11] * 1.0;
-      this->out_pin_level_ = find_str(OUT_PIN_LEVELS_BY_UINT, buffer[12]);
-      ESP_LOGV(TAG, "Light function: %s", const_cast(this->light_function_.c_str()));
-      ESP_LOGV(TAG, "Light threshold: %f", this->light_threshold_);
-      ESP_LOGV(TAG, "Out pin level: %s", const_cast(this->out_pin_level_.c_str()));
+      break;
+    }
+
+    case CMD_QUERY_LIGHT_CONTROL: {
+      this->light_function_ = this->buffer_data_[10];
+      this->light_threshold_ = this->buffer_data_[11];
+      this->out_pin_level_ = this->buffer_data_[12];
+      const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_);
+      const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_);
+      ESP_LOGV(TAG,
+               "Light function is: %s\n"
+               "Light threshold is: %u\n"
+               "Out pin level: %s",
+               light_function_str, this->light_threshold_, out_pin_level_str);
 #ifdef USE_SELECT
-      if (this->light_function_select_ != nullptr && this->light_function_select_->state != this->light_function_) {
-        this->light_function_select_->publish_state(this->light_function_);
+      if (this->light_function_select_ != nullptr) {
+        this->light_function_select_->publish_state(light_function_str);
       }
-      if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->state != this->out_pin_level_) {
-        this->out_pin_level_select_->publish_state(this->out_pin_level_);
+      if (this->out_pin_level_select_ != nullptr) {
+        this->out_pin_level_select_->publish_state(out_pin_level_str);
       }
 #endif
 #ifdef USE_NUMBER
-      if (this->light_threshold_number_ != nullptr &&
-          (!this->light_threshold_number_->has_state() ||
-           this->light_threshold_number_->state != this->light_threshold_)) {
-        this->light_threshold_number_->publish_state(this->light_threshold_);
+      if (this->light_threshold_number_ != nullptr) {
+        this->light_threshold_number_->publish_state(static_cast(this->light_threshold_));
       }
 #endif
-    } break;
-    case lowbyte(CMD_MAC):
-      if (len < 20) {
+      break;
+    }
+    case CMD_QUERY_MAC_ADDRESS: {
+      if (this->buffer_pos_ < 20) {
         return false;
       }
-      this->mac_ = format_mac_address_pretty(&buffer[10]);
-      ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str());
+
+      this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
+      if (this->bluetooth_on_) {
+        std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
+      }
+
+      std::string mac_str =
+          mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
+      ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str());
 #ifdef USE_TEXT_SENSOR
       if (this->mac_text_sensor_ != nullptr) {
-        this->mac_text_sensor_->publish_state(this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_);
+        this->mac_text_sensor_->publish_state(mac_str);
       }
 #endif
 #ifdef USE_SWITCH
       if (this->bluetooth_switch_ != nullptr) {
-        this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC);
+        this->bluetooth_switch_->publish_state(this->bluetooth_on_);
       }
 #endif
       break;
-    case lowbyte(CMD_GATE_SENS):
+    }
+
+    case CMD_GATE_SENS:
       ESP_LOGV(TAG, "Sensitivity");
       break;
-    case lowbyte(CMD_BLUETOOTH):
+
+    case CMD_BLUETOOTH:
       ESP_LOGV(TAG, "Bluetooth");
       break;
-    case lowbyte(CMD_SET_DISTANCE_RESOLUTION):
+
+    case CMD_SET_DISTANCE_RESOLUTION:
       ESP_LOGV(TAG, "Set distance resolution");
       break;
-    case lowbyte(CMD_SET_LIGHT_CONTROL):
+
+    case CMD_SET_LIGHT_CONTROL:
       ESP_LOGV(TAG, "Set light control");
       break;
-    case lowbyte(CMD_BT_PASSWORD):
+
+    case CMD_BT_PASSWORD:
       ESP_LOGV(TAG, "Set bluetooth password");
       break;
-    case lowbyte(CMD_QUERY):  // Query parameters response
-    {
-      if (buffer[10] != 0xAA)
+
+    case CMD_QUERY: {  // Query parameters response
+      if (this->buffer_data_[10] != 0xAA)
         return true;  // value head=0xAA
 #ifdef USE_NUMBER
       /*
@@ -567,29 +608,31 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
         Still distance range: 14th byte
       */
       std::vector> updates;
-      updates.push_back(set_number_value(this->max_move_distance_gate_number_, buffer[12]));
-      updates.push_back(set_number_value(this->max_still_distance_gate_number_, buffer[13]));
+      updates.push_back(set_number_value(this->max_move_distance_gate_number_, this->buffer_data_[12]));
+      updates.push_back(set_number_value(this->max_still_distance_gate_number_, this->buffer_data_[13]));
       /*
         Moving Sensitivities: 15~23th bytes
       */
       for (std::vector::size_type i = 0; i != this->gate_move_threshold_numbers_.size(); i++) {
-        updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], buffer[14 + i]));
+        updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[14 + i]));
       }
       /*
         Still Sensitivities: 24~32th bytes
       */
       for (std::vector::size_type i = 0; i != this->gate_still_threshold_numbers_.size(); i++) {
-        updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], buffer[23 + i]));
+        updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[23 + i]));
       }
       /*
         None Duration: 33~34th bytes
       */
-      updates.push_back(set_number_value(this->timeout_number_, ld2410::two_byte_to_int(buffer[32], buffer[33])));
+      updates.push_back(set_number_value(this->timeout_number_,
+                                         ld2410::two_byte_to_int(this->buffer_data_[32], this->buffer_data_[33])));
       for (auto &update : updates) {
         update();
       }
 #endif
-    } break;
+      break;
+    }
     default:
       break;
   }
@@ -597,59 +640,66 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
   return true;
 }
 
-void LD2410Component::readline_(int readch, uint8_t *buffer, int len) {
-  static int pos = 0;
+void LD2410Component::readline_(int readch) {
+  if (readch < 0) {
+    return;  // No data available
+  }
 
-  if (readch >= 0) {
-    if (pos < len - 1) {
-      buffer[pos++] = readch;
-      buffer[pos] = 0;
+  if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) {
+    this->buffer_data_[this->buffer_pos_++] = readch;
+    this->buffer_data_[this->buffer_pos_] = 0;
+  } else {
+    // We should never get here, but just in case...
+    ESP_LOGW(TAG, "Max command length exceeded; ignoring");
+    this->buffer_pos_ = 0;
+  }
+  if (this->buffer_pos_ < 4) {
+    return;  // Not enough data to process yet
+  }
+  if (this->buffer_data_[this->buffer_pos_ - 4] == DATA_FRAME_FOOTER[0] &&
+      this->buffer_data_[this->buffer_pos_ - 3] == DATA_FRAME_FOOTER[1] &&
+      this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[2] &&
+      this->buffer_data_[this->buffer_pos_ - 1] == DATA_FRAME_FOOTER[3]) {
+    ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str());
+    this->handle_periodic_data_();
+    this->buffer_pos_ = 0;  // Reset position index for next message
+  } else if (this->buffer_data_[this->buffer_pos_ - 4] == CMD_FRAME_FOOTER[0] &&
+             this->buffer_data_[this->buffer_pos_ - 3] == CMD_FRAME_FOOTER[1] &&
+             this->buffer_data_[this->buffer_pos_ - 2] == CMD_FRAME_FOOTER[2] &&
+             this->buffer_data_[this->buffer_pos_ - 1] == CMD_FRAME_FOOTER[3]) {
+    ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str());
+    if (this->handle_ack_data_()) {
+      this->buffer_pos_ = 0;  // Reset position index for next message
     } else {
-      pos = 0;
-    }
-    if (pos >= 4) {
-      if (buffer[pos - 4] == 0xF8 && buffer[pos - 3] == 0xF7 && buffer[pos - 2] == 0xF6 && buffer[pos - 1] == 0xF5) {
-        ESP_LOGV(TAG, "Will handle Periodic Data");
-        this->handle_periodic_data_(buffer, pos);
-        pos = 0;  // Reset position index ready for next time
-      } else if (buffer[pos - 4] == 0x04 && buffer[pos - 3] == 0x03 && buffer[pos - 2] == 0x02 &&
-                 buffer[pos - 1] == 0x01) {
-        ESP_LOGV(TAG, "Will handle ACK Data");
-        if (this->handle_ack_data_(buffer, pos)) {
-          pos = 0;  // Reset position index ready for next time
-        } else {
-          ESP_LOGV(TAG, "ACK Data incomplete");
-        }
-      }
+      ESP_LOGV(TAG, "Ack Data incomplete");
     }
   }
 }
 
 void LD2410Component::set_config_mode_(bool enable) {
-  uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
-  uint8_t cmd_value[2] = {0x01, 0x00};
-  this->send_command_(cmd, enable ? cmd_value : nullptr, 2);
+  const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
+  const uint8_t cmd_value[2] = {0x01, 0x00};
+  this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value));
 }
 
 void LD2410Component::set_bluetooth(bool enable) {
   this->set_config_mode_(true);
-  uint8_t enable_cmd_value[2] = {0x01, 0x00};
-  uint8_t disable_cmd_value[2] = {0x00, 0x00};
-  this->send_command_(CMD_BLUETOOTH, enable ? enable_cmd_value : disable_cmd_value, 2);
+  const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00};
+  this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value));
   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
 }
 
 void LD2410Component::set_distance_resolution(const std::string &state) {
   this->set_config_mode_(true);
-  uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00};
-  this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, 2);
+  const uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00};
+  this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value));
   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
 }
 
 void LD2410Component::set_baud_rate(const std::string &state) {
   this->set_config_mode_(true);
-  uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
-  this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2);
+  const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
+  this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
   this->set_timeout(200, [this]() { this->restart_(); });
 }
 
@@ -661,14 +711,13 @@ void LD2410Component::set_bluetooth_password(const std::string &password) {
   this->set_config_mode_(true);
   uint8_t cmd_value[6];
   std::copy(password.begin(), password.end(), std::begin(cmd_value));
-  this->send_command_(CMD_BT_PASSWORD, cmd_value, 6);
+  this->send_command_(CMD_BT_PASSWORD, cmd_value, sizeof(cmd_value));
   this->set_config_mode_(false);
 }
 
 void LD2410Component::set_engineering_mode(bool enable) {
+  const uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG;
   this->set_config_mode_(true);
-  last_engineering_mode_change_millis_ = App.get_loop_component_start_time();
-  uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG;
   this->send_command_(cmd, nullptr, 0);
   this->set_config_mode_(false);
 }
@@ -682,14 +731,17 @@ void LD2410Component::factory_reset() {
 void LD2410Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); }
 
 void LD2410Component::query_parameters_() { this->send_command_(CMD_QUERY, nullptr, 0); }
-void LD2410Component::get_version_() { this->send_command_(CMD_VERSION, nullptr, 0); }
+
+void LD2410Component::get_version_() { this->send_command_(CMD_QUERY_VERSION, nullptr, 0); }
+
 void LD2410Component::get_mac_() {
-  uint8_t cmd_value[2] = {0x01, 0x00};
-  this->send_command_(CMD_MAC, cmd_value, 2);
+  const uint8_t cmd_value[2] = {0x01, 0x00};
+  this->send_command_(CMD_QUERY_MAC_ADDRESS, cmd_value, sizeof(cmd_value));
 }
+
 void LD2410Component::get_distance_resolution_() { this->send_command_(CMD_QUERY_DISTANCE_RESOLUTION, nullptr, 0); }
 
-void LD2410Component::get_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); }
+void LD2410Component::query_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); }
 
 #ifdef USE_NUMBER
 void LD2410Component::set_max_distances_timeout() {
@@ -719,7 +771,7 @@ void LD2410Component::set_max_distances_timeout() {
                        0x00,
                        0x00};
   this->set_config_mode_(true);
-  this->send_command_(CMD_MAXDIST_DURATION, value, 18);
+  this->send_command_(CMD_MAXDIST_DURATION, value, sizeof(value));
   delay(50);  // NOLINT
   this->query_parameters_();
   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
@@ -749,17 +801,17 @@ void LD2410Component::set_gate_threshold(uint8_t gate) {
   uint8_t value[18] = {0x00, 0x00, lowbyte(gate),   highbyte(gate),   0x00, 0x00,
                        0x01, 0x00, lowbyte(motion), highbyte(motion), 0x00, 0x00,
                        0x02, 0x00, lowbyte(still),  highbyte(still),  0x00, 0x00};
-  this->send_command_(CMD_GATE_SENS, value, 18);
+  this->send_command_(CMD_GATE_SENS, value, sizeof(value));
   delay(50);  // NOLINT
   this->query_parameters_();
   this->set_config_mode_(false);
 }
 
-void LD2410Component::set_gate_still_threshold_number(int gate, number::Number *n) {
+void LD2410Component::set_gate_still_threshold_number(uint8_t gate, number::Number *n) {
   this->gate_still_threshold_numbers_[gate] = n;
 }
 
-void LD2410Component::set_gate_move_threshold_number(int gate, number::Number *n) {
+void LD2410Component::set_gate_move_threshold_number(uint8_t gate, number::Number *n) {
   this->gate_move_threshold_numbers_[gate] = n;
 }
 #endif
@@ -767,35 +819,29 @@ void LD2410Component::set_gate_move_threshold_number(int gate, number::Number *n
 void LD2410Component::set_light_out_control() {
 #ifdef USE_NUMBER
   if (this->light_threshold_number_ != nullptr && this->light_threshold_number_->has_state()) {
-    this->light_threshold_ = this->light_threshold_number_->state;
+    this->light_threshold_ = static_cast(this->light_threshold_number_->state);
   }
 #endif
 #ifdef USE_SELECT
   if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) {
-    this->light_function_ = this->light_function_select_->state;
+    this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->state);
   }
   if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) {
-    this->out_pin_level_ = this->out_pin_level_select_->state;
+    this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->state);
   }
 #endif
-  if (this->light_function_.empty() || this->out_pin_level_.empty() || this->light_threshold_ < 0) {
-    return;
-  }
   this->set_config_mode_(true);
-  uint8_t light_function = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_);
-  uint8_t light_threshold = static_cast(this->light_threshold_);
-  uint8_t out_pin_level = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_);
-  uint8_t value[4] = {light_function, light_threshold, out_pin_level, 0x00};
-  this->send_command_(CMD_SET_LIGHT_CONTROL, value, 4);
+  uint8_t value[4] = {this->light_function_, this->light_threshold_, this->out_pin_level_, 0x00};
+  this->send_command_(CMD_SET_LIGHT_CONTROL, value, sizeof(value));
   delay(50);  // NOLINT
-  this->get_light_control_();
+  this->query_light_control_();
   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
   this->set_config_mode_(false);
 }
 
 #ifdef USE_SENSOR
-void LD2410Component::set_gate_move_sensor(int gate, sensor::Sensor *s) { this->gate_move_sensors_[gate] = s; }
-void LD2410Component::set_gate_still_sensor(int gate, sensor::Sensor *s) { this->gate_still_sensors_[gate] = s; }
+void LD2410Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) { this->gate_move_sensors_[gate] = s; }
+void LD2410Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { this->gate_still_sensors_[gate] = s; }
 #endif
 
 }  // namespace ld2410
diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h
index 1b5f6e3057..8bd1dbcb5a 100644
--- a/esphome/components/ld2410/ld2410.h
+++ b/esphome/components/ld2410/ld2410.h
@@ -29,45 +29,48 @@
 namespace esphome {
 namespace ld2410 {
 
+static const uint8_t MAX_LINE_LENGTH = 46;  // Max characters for serial buffer
+static const uint8_t TOTAL_GATES = 9;       // Total number of gates supported by the LD2410
+
 class LD2410Component : public Component, public uart::UARTDevice {
-#ifdef USE_SENSOR
-  SUB_SENSOR(moving_target_distance)
-  SUB_SENSOR(still_target_distance)
-  SUB_SENSOR(moving_target_energy)
-  SUB_SENSOR(still_target_energy)
-  SUB_SENSOR(light)
-  SUB_SENSOR(detection_distance)
-#endif
 #ifdef USE_BINARY_SENSOR
-  SUB_BINARY_SENSOR(target)
+  SUB_BINARY_SENSOR(out_pin_presence_status)
   SUB_BINARY_SENSOR(moving_target)
   SUB_BINARY_SENSOR(still_target)
-  SUB_BINARY_SENSOR(out_pin_presence_status)
+  SUB_BINARY_SENSOR(target)
+#endif
+#ifdef USE_SENSOR
+  SUB_SENSOR(light)
+  SUB_SENSOR(detection_distance)
+  SUB_SENSOR(moving_target_distance)
+  SUB_SENSOR(moving_target_energy)
+  SUB_SENSOR(still_target_distance)
+  SUB_SENSOR(still_target_energy)
 #endif
 #ifdef USE_TEXT_SENSOR
   SUB_TEXT_SENSOR(version)
   SUB_TEXT_SENSOR(mac)
 #endif
+#ifdef USE_NUMBER
+  SUB_NUMBER(light_threshold)
+  SUB_NUMBER(max_move_distance_gate)
+  SUB_NUMBER(max_still_distance_gate)
+  SUB_NUMBER(timeout)
+#endif
 #ifdef USE_SELECT
-  SUB_SELECT(distance_resolution)
   SUB_SELECT(baud_rate)
+  SUB_SELECT(distance_resolution)
   SUB_SELECT(light_function)
   SUB_SELECT(out_pin_level)
 #endif
 #ifdef USE_SWITCH
-  SUB_SWITCH(engineering_mode)
   SUB_SWITCH(bluetooth)
+  SUB_SWITCH(engineering_mode)
 #endif
 #ifdef USE_BUTTON
-  SUB_BUTTON(reset)
-  SUB_BUTTON(restart)
+  SUB_BUTTON(factory_reset)
   SUB_BUTTON(query)
-#endif
-#ifdef USE_NUMBER
-  SUB_NUMBER(max_still_distance_gate)
-  SUB_NUMBER(max_move_distance_gate)
-  SUB_NUMBER(timeout)
-  SUB_NUMBER(light_threshold)
+  SUB_BUTTON(restart)
 #endif
 
  public:
@@ -76,14 +79,14 @@ class LD2410Component : public Component, public uart::UARTDevice {
   void loop() override;
   void set_light_out_control();
 #ifdef USE_NUMBER
-  void set_gate_still_threshold_number(int gate, number::Number *n);
-  void set_gate_move_threshold_number(int gate, number::Number *n);
+  void set_gate_still_threshold_number(uint8_t gate, number::Number *n);
+  void set_gate_move_threshold_number(uint8_t gate, number::Number *n);
   void set_max_distances_timeout();
   void set_gate_threshold(uint8_t gate);
 #endif
 #ifdef USE_SENSOR
-  void set_gate_move_sensor(int gate, sensor::Sensor *s);
-  void set_gate_still_sensor(int gate, sensor::Sensor *s);
+  void set_gate_move_sensor(uint8_t gate, sensor::Sensor *s);
+  void set_gate_still_sensor(uint8_t gate, sensor::Sensor *s);
 #endif
   void set_throttle(uint16_t value) { this->throttle_ = value; };
   void set_bluetooth_password(const std::string &password);
@@ -96,33 +99,35 @@ class LD2410Component : public Component, public uart::UARTDevice {
   void factory_reset();
 
  protected:
-  void send_command_(uint8_t command_str, const uint8_t *command_value, int command_value_len);
+  void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
   void set_config_mode_(bool enable);
-  void handle_periodic_data_(uint8_t *buffer, int len);
-  bool handle_ack_data_(uint8_t *buffer, int len);
-  void readline_(int readch, uint8_t *buffer, int len);
+  void handle_periodic_data_();
+  bool handle_ack_data_();
+  void readline_(int readch);
   void query_parameters_();
   void get_version_();
   void get_mac_();
   void get_distance_resolution_();
-  void get_light_control_();
+  void query_light_control_();
   void restart_();
 
-  int32_t last_periodic_millis_ = 0;
-  int32_t last_engineering_mode_change_millis_ = 0;
-  uint16_t throttle_;
-  float light_threshold_ = -1;
-  std::string version_;
-  std::string mac_;
-  std::string out_pin_level_;
-  std::string light_function_;
+  uint32_t last_periodic_millis_ = 0;
+  uint16_t throttle_ = 0;
+  uint8_t light_function_ = 0;
+  uint8_t light_threshold_ = 0;
+  uint8_t out_pin_level_ = 0;
+  uint8_t buffer_pos_ = 0;  // where to resume processing/populating buffer
+  uint8_t buffer_data_[MAX_LINE_LENGTH];
+  uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0};
+  uint8_t version_[6] = {0, 0, 0, 0, 0, 0};
+  bool bluetooth_on_{false};
 #ifdef USE_NUMBER
-  std::vector gate_still_threshold_numbers_ = std::vector(9);
-  std::vector gate_move_threshold_numbers_ = std::vector(9);
+  std::vector gate_move_threshold_numbers_ = std::vector(TOTAL_GATES);
+  std::vector gate_still_threshold_numbers_ = std::vector(TOTAL_GATES);
 #endif
 #ifdef USE_SENSOR
-  std::vector gate_still_sensors_ = std::vector(9);
-  std::vector gate_move_sensors_ = std::vector(9);
+  std::vector gate_move_sensors_ = std::vector(TOTAL_GATES);
+  std::vector gate_still_sensors_ = std::vector(TOTAL_GATES);
 #endif
 };
 

From 8da322fe9efe96e0128af82ae312eddc6df64071 Mon Sep 17 00:00:00 2001
From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Date: Sun, 6 Jul 2025 14:04:43 -0400
Subject: [PATCH 242/293] [sx127x] Improve error handling (#9351)

---
 esphome/components/sx127x/sx127x.cpp | 24 +++++++++++++-----------
 esphome/components/sx127x/sx127x.h   |  4 +++-
 2 files changed, 16 insertions(+), 12 deletions(-)

diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp
index e41efe098c..7f62ee2bd3 100644
--- a/esphome/components/sx127x/sx127x.cpp
+++ b/esphome/components/sx127x/sx127x.cpp
@@ -252,15 +252,17 @@ size_t SX127x::get_max_packet_size() {
   }
 }
 
-void SX127x::transmit_packet(const std::vector &packet) {
+SX127xError SX127x::transmit_packet(const std::vector &packet) {
   if (this->payload_length_ > 0 && this->payload_length_ != packet.size()) {
     ESP_LOGE(TAG, "Packet size does not match config");
-    return;
+    return SX127xError::INVALID_PARAMS;
   }
   if (packet.empty() || packet.size() > this->get_max_packet_size()) {
     ESP_LOGE(TAG, "Packet size out of range");
-    return;
+    return SX127xError::INVALID_PARAMS;
   }
+
+  SX127xError ret = SX127xError::NONE;
   if (this->modulation_ == MOD_LORA) {
     this->set_mode_standby();
     if (this->payload_length_ == 0) {
@@ -278,11 +280,13 @@ void SX127x::transmit_packet(const std::vector &packet) {
     this->write_fifo_(packet);
     this->set_mode_tx();
   }
+
   // wait until transmit completes, typically the delay will be less than 100 ms
   uint32_t start = millis();
   while (!this->dio0_pin_->digital_read()) {
     if (millis() - start > 4000) {
       ESP_LOGE(TAG, "Transmit packet failure");
+      ret = SX127xError::TIMEOUT;
       break;
     }
   }
@@ -291,6 +295,7 @@ void SX127x::transmit_packet(const std::vector &packet) {
   } else {
     this->set_mode_sleep();
   }
+  return ret;
 }
 
 void SX127x::call_listeners_(const std::vector &packet, float rssi, float snr) {
@@ -335,13 +340,7 @@ void SX127x::loop() {
 }
 
 void SX127x::run_image_cal() {
-  uint32_t start = millis();
-  uint8_t mode = this->read_register_(REG_OP_MODE);
-  if ((mode & MODE_MASK) != MODE_STDBY) {
-    ESP_LOGE(TAG, "Need to be in standby for image cal");
-    return;
-  }
-  if (mode & MOD_LORA) {
+  if (this->modulation_ == MOD_LORA) {
     this->set_mode_(MOD_FSK, MODE_SLEEP);
     this->set_mode_(MOD_FSK, MODE_STDBY);
   }
@@ -350,13 +349,15 @@ void SX127x::run_image_cal() {
   } else {
     this->write_register_(REG_IMAGE_CAL, IMAGE_CAL_START);
   }
+  uint32_t start = millis();
   while (this->read_register_(REG_IMAGE_CAL) & IMAGE_CAL_RUNNING) {
     if (millis() - start > 20) {
       ESP_LOGE(TAG, "Image cal failure");
+      this->mark_failed();
       break;
     }
   }
-  if (mode & MOD_LORA) {
+  if (this->modulation_ == MOD_LORA) {
     this->set_mode_(this->modulation_, MODE_SLEEP);
     this->set_mode_(this->modulation_, MODE_STDBY);
   }
@@ -375,6 +376,7 @@ void SX127x::set_mode_(uint8_t modulation, uint8_t mode) {
     }
     if (millis() - start > 20) {
       ESP_LOGE(TAG, "Set mode failure");
+      this->mark_failed();
       break;
     }
   }
diff --git a/esphome/components/sx127x/sx127x.h b/esphome/components/sx127x/sx127x.h
index fe9f60e860..4cc7c9b6d3 100644
--- a/esphome/components/sx127x/sx127x.h
+++ b/esphome/components/sx127x/sx127x.h
@@ -34,6 +34,8 @@ enum SX127xBw : uint8_t {
   SX127X_BW_500_0,
 };
 
+enum class SX127xError { NONE = 0, TIMEOUT, INVALID_PARAMS };
+
 class SX127xListener {
  public:
   virtual void on_packet(const std::vector &packet, float rssi, float snr) = 0;
@@ -79,7 +81,7 @@ class SX127x : public Component,
   void set_sync_value(const std::vector &sync_value) { this->sync_value_ = sync_value; }
   void run_image_cal();
   void configure();
-  void transmit_packet(const std::vector &packet);
+  SX127xError transmit_packet(const std::vector &packet);
   void register_listener(SX127xListener *listener) { this->listeners_.push_back(listener); }
   Trigger, float, float> *get_packet_trigger() const { return this->packet_trigger_; };
 

From b6fade7339f941e9d08caa14188bc2928259cf80 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Sun, 6 Jul 2025 17:01:51 -0500
Subject: [PATCH 243/293] Fix defer() thread safety issues on multi-core
 platforms (#9317)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
 esphome/components/web_server/web_server.cpp  |  86 +++--------
 esphome/components/web_server/web_server.h    |  12 --
 esphome/core/helpers.cpp                      |   9 +-
 esphome/core/helpers.h                        |   4 +
 esphome/core/scheduler.cpp                    | 103 ++++++++++---
 esphome/core/scheduler.h                      |  21 +++
 .../fixtures/defer_fifo_simple.yaml           | 109 ++++++++++++++
 tests/integration/fixtures/defer_stress.yaml  |  38 +++++
 .../defer_stress_component/__init__.py        |  19 +++
 .../defer_stress_component.cpp                |  75 ++++++++++
 .../defer_stress_component.h                  |  20 +++
 tests/integration/test_defer_fifo_simple.py   | 117 +++++++++++++++
 tests/integration/test_defer_stress.py        | 137 ++++++++++++++++++
 13 files changed, 654 insertions(+), 96 deletions(-)
 create mode 100644 tests/integration/fixtures/defer_fifo_simple.yaml
 create mode 100644 tests/integration/fixtures/defer_stress.yaml
 create mode 100644 tests/integration/fixtures/external_components/defer_stress_component/__init__.py
 create mode 100644 tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.cpp
 create mode 100644 tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.h
 create mode 100644 tests/integration/test_defer_fifo_simple.py
 create mode 100644 tests/integration/test_defer_stress.py

diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp
index 77c20b956b..20ff1a7c29 100644
--- a/esphome/components/web_server/web_server.cpp
+++ b/esphome/components/web_server/web_server.cpp
@@ -255,11 +255,7 @@ void DeferredUpdateEventSourceList::on_client_disconnect_(DeferredUpdateEventSou
 }
 #endif
 
-WebServer::WebServer(web_server_base::WebServerBase *base) : base_(base) {
-#ifdef USE_ESP32
-  to_schedule_lock_ = xSemaphoreCreateMutex();
-#endif
-}
+WebServer::WebServer(web_server_base::WebServerBase *base) : base_(base) {}
 
 #ifdef USE_WEBSERVER_CSS_INCLUDE
 void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; }
@@ -308,30 +304,7 @@ void WebServer::setup() {
   // getting a lot of events
   this->set_interval(10000, [this]() { this->events_.try_send_nodefer("", "ping", millis(), 30000); });
 }
-void WebServer::loop() {
-#ifdef USE_ESP32
-  // Check atomic flag first to avoid taking semaphore when queue is empty
-  if (this->to_schedule_has_items_.load(std::memory_order_relaxed) && xSemaphoreTake(this->to_schedule_lock_, 0L)) {
-    std::function fn;
-    if (!to_schedule_.empty()) {
-      // scheduler execute things out of order which may lead to incorrect state
-      // this->defer(std::move(to_schedule_.front()));
-      // let's execute it directly from the loop
-      fn = std::move(to_schedule_.front());
-      to_schedule_.pop_front();
-      if (to_schedule_.empty()) {
-        this->to_schedule_has_items_.store(false, std::memory_order_relaxed);
-      }
-    }
-    xSemaphoreGive(this->to_schedule_lock_);
-    if (fn) {
-      fn();
-    }
-  }
-#endif
-
-  this->events_.loop();
-}
+void WebServer::loop() { this->events_.loop(); }
 void WebServer::dump_config() {
   ESP_LOGCONFIG(TAG,
                 "Web Server:\n"
@@ -526,13 +499,13 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
       std::string data = this->switch_json(obj, obj->state, detail);
       request->send(200, "application/json", data.c_str());
     } else if (match.method_equals("toggle")) {
-      this->schedule_([obj]() { obj->toggle(); });
+      this->defer([obj]() { obj->toggle(); });
       request->send(200);
     } else if (match.method_equals("turn_on")) {
-      this->schedule_([obj]() { obj->turn_on(); });
+      this->defer([obj]() { obj->turn_on(); });
       request->send(200);
     } else if (match.method_equals("turn_off")) {
-      this->schedule_([obj]() { obj->turn_off(); });
+      this->defer([obj]() { obj->turn_off(); });
       request->send(200);
     } else {
       request->send(404);
@@ -568,7 +541,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
       std::string data = this->button_json(obj, detail);
       request->send(200, "application/json", data.c_str());
     } else if (match.method_equals("press")) {
-      this->schedule_([obj]() { obj->press(); });
+      this->defer([obj]() { obj->press(); });
       request->send(200);
       return;
     } else {
@@ -648,7 +621,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
       std::string data = this->fan_json(obj, detail);
       request->send(200, "application/json", data.c_str());
     } else if (match.method_equals("toggle")) {
-      this->schedule_([obj]() { obj->toggle().perform(); });
+      this->defer([obj]() { obj->toggle().perform(); });
       request->send(200);
     } else if (match.method_equals("turn_on") || match.method_equals("turn_off")) {
       auto call = match.method_equals("turn_on") ? obj->turn_on() : obj->turn_off();
@@ -680,7 +653,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
             return;
         }
       }
-      this->schedule_([call]() mutable { call.perform(); });
+      this->defer([call]() mutable { call.perform(); });
       request->send(200);
     } else {
       request->send(404);
@@ -729,7 +702,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
       std::string data = this->light_json(obj, detail);
       request->send(200, "application/json", data.c_str());
     } else if (match.method_equals("toggle")) {
-      this->schedule_([obj]() { obj->toggle().perform(); });
+      this->defer([obj]() { obj->toggle().perform(); });
       request->send(200);
     } else if (match.method_equals("turn_on")) {
       auto call = obj->turn_on();
@@ -786,7 +759,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
         call.set_effect(effect);
       }
 
-      this->schedule_([call]() mutable { call.perform(); });
+      this->defer([call]() mutable { call.perform(); });
       request->send(200);
     } else if (match.method_equals("turn_off")) {
       auto call = obj->turn_off();
@@ -796,7 +769,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
           call.set_transition_length(*transition * 1000);
         }
       }
-      this->schedule_([call]() mutable { call.perform(); });
+      this->defer([call]() mutable { call.perform(); });
       request->send(200);
     } else {
       request->send(404);
@@ -881,7 +854,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
       }
     }
 
-    this->schedule_([call]() mutable { call.perform(); });
+    this->defer([call]() mutable { call.perform(); });
     request->send(200);
     return;
   }
@@ -939,7 +912,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
         call.set_value(*value);
     }
 
-    this->schedule_([call]() mutable { call.perform(); });
+    this->defer([call]() mutable { call.perform(); });
     request->send(200);
     return;
   }
@@ -1014,7 +987,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
       call.set_date(value);
     }
 
-    this->schedule_([call]() mutable { call.perform(); });
+    this->defer([call]() mutable { call.perform(); });
     request->send(200);
     return;
   }
@@ -1073,7 +1046,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
       call.set_time(value);
     }
 
-    this->schedule_([call]() mutable { call.perform(); });
+    this->defer([call]() mutable { call.perform(); });
     request->send(200);
     return;
   }
@@ -1131,7 +1104,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
       call.set_datetime(value);
     }
 
-    this->schedule_([call]() mutable { call.perform(); });
+    this->defer([call]() mutable { call.perform(); });
     request->send(200);
     return;
   }
@@ -1248,7 +1221,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
       call.set_option(option.c_str());  // NOLINT
     }
 
-    this->schedule_([call]() mutable { call.perform(); });
+    this->defer([call]() mutable { call.perform(); });
     request->send(200);
     return;
   }
@@ -1335,7 +1308,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
         call.set_target_temperature(*target_temperature);
     }
 
-    this->schedule_([call]() mutable { call.perform(); });
+    this->defer([call]() mutable { call.perform(); });
     request->send(200);
     return;
   }
@@ -1452,13 +1425,13 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
       std::string data = this->lock_json(obj, obj->state, detail);
       request->send(200, "application/json", data.c_str());
     } else if (match.method_equals("lock")) {
-      this->schedule_([obj]() { obj->lock(); });
+      this->defer([obj]() { obj->lock(); });
       request->send(200);
     } else if (match.method_equals("unlock")) {
-      this->schedule_([obj]() { obj->unlock(); });
+      this->defer([obj]() { obj->unlock(); });
       request->send(200);
     } else if (match.method_equals("open")) {
-      this->schedule_([obj]() { obj->open(); });
+      this->defer([obj]() { obj->open(); });
       request->send(200);
     } else {
       request->send(404);
@@ -1529,7 +1502,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
       }
     }
 
-    this->schedule_([call]() mutable { call.perform(); });
+    this->defer([call]() mutable { call.perform(); });
     request->send(200);
     return;
   }
@@ -1594,7 +1567,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
       return;
     }
 
-    this->schedule_([call]() mutable { call.perform(); });
+    this->defer([call]() mutable { call.perform(); });
     request->send(200);
     return;
   }
@@ -1695,7 +1668,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM
       return;
     }
 
-    this->schedule_([obj]() mutable { obj->perform(); });
+    this->defer([obj]() mutable { obj->perform(); });
     request->send(200);
     return;
   }
@@ -2072,17 +2045,6 @@ void WebServer::add_sorting_group(uint64_t group_id, const std::string &group_na
 }
 #endif
 
-void WebServer::schedule_(std::function &&f) {
-#ifdef USE_ESP32
-  xSemaphoreTake(this->to_schedule_lock_, portMAX_DELAY);
-  to_schedule_.push_back(std::move(f));
-  this->to_schedule_has_items_.store(true, std::memory_order_relaxed);
-  xSemaphoreGive(this->to_schedule_lock_);
-#else
-  this->defer(std::move(f));
-#endif
-}
-
 }  // namespace web_server
 }  // namespace esphome
 #endif
diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h
index 82b31ab656..0c15881d1e 100644
--- a/esphome/components/web_server/web_server.h
+++ b/esphome/components/web_server/web_server.h
@@ -14,12 +14,6 @@
 #include 
 #include 
 #include 
-#ifdef USE_ESP32
-#include 
-#include 
-#include 
-#include 
-#endif
 
 #if USE_WEBSERVER_VERSION >= 2
 extern const uint8_t ESPHOME_WEBSERVER_INDEX_HTML[] PROGMEM;
@@ -504,7 +498,6 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
 
  protected:
   void add_sorting_info_(JsonObject &root, EntityBase *entity);
-  void schedule_(std::function &&f);
   web_server_base::WebServerBase *base_;
 #ifdef USE_ARDUINO
   DeferredUpdateEventSourceList events_;
@@ -524,11 +517,6 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
   const char *js_include_{nullptr};
 #endif
   bool expose_log_{true};
-#ifdef USE_ESP32
-  std::deque> to_schedule_;
-  SemaphoreHandle_t to_schedule_lock_;
-  std::atomic to_schedule_has_items_{false};
-#endif
 };
 
 }  // namespace web_server
diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp
index b4923c7af0..7d9b86fccd 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -645,7 +645,7 @@ void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green
 }
 
 // System APIs
-#if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_HOST)
+#if defined(USE_ESP8266) || defined(USE_RP2040)
 // ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS.
 Mutex::Mutex() {}
 Mutex::~Mutex() {}
@@ -658,6 +658,13 @@ Mutex::~Mutex() {}
 void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
 bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
 void Mutex::unlock() { xSemaphoreGive(this->handle_); }
+#elif defined(USE_HOST)
+// Host platform uses std::mutex for proper thread synchronization
+Mutex::Mutex() { handle_ = new std::mutex(); }
+Mutex::~Mutex() { delete static_cast(handle_); }
+void Mutex::lock() { static_cast(handle_)->lock(); }
+bool Mutex::try_lock() { return static_cast(handle_)->try_lock(); }
+void Mutex::unlock() { static_cast(handle_)->unlock(); }
 #endif
 
 #if defined(USE_ESP8266)
diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h
index 362f3d1fa4..d92cf07702 100644
--- a/esphome/core/helpers.h
+++ b/esphome/core/helpers.h
@@ -32,6 +32,10 @@
 #include 
 #endif
 
+#ifdef USE_HOST
+#include 
+#endif
+
 #define HOT __attribute__((hot))
 #define ESPDEPRECATED(msg, when) __attribute__((deprecated(msg)))
 #define ESPHOME_ALWAYS_INLINE __attribute__((always_inline))
diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp
index 5c01b4f3f4..515f6fd355 100644
--- a/esphome/core/scheduler.cpp
+++ b/esphome/core/scheduler.cpp
@@ -73,8 +73,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
   if (delay == SCHEDULER_DONT_RUN)
     return;
 
-  const auto now = this->millis_();
-
   // Create and populate the scheduler item
   auto item = make_unique();
   item->component = component;
@@ -83,6 +81,19 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
   item->callback = std::move(func);
   item->remove = false;
 
+#if !defined(USE_ESP8266) && !defined(USE_RP2040)
+  // Special handling for defer() (delay = 0, type = TIMEOUT)
+  // ESP8266 and RP2040 are excluded because they don't need thread-safe defer handling
+  if (delay == 0 && type == SchedulerItem::TIMEOUT) {
+    // Put in defer queue for guaranteed FIFO execution
+    LockGuard guard{this->lock_};
+    this->defer_queue_.push_back(std::move(item));
+    return;
+  }
+#endif
+
+  const auto now = this->millis_();
+
   // Type-specific setup
   if (type == SchedulerItem::INTERVAL) {
     item->interval = delay;
@@ -209,6 +220,35 @@ optional HOT Scheduler::next_schedule_in() {
   return item->next_execution_ - now;
 }
 void HOT Scheduler::call() {
+#if !defined(USE_ESP8266) && !defined(USE_RP2040)
+  // Process defer queue first to guarantee FIFO execution order for deferred items.
+  // Previously, defer() used the heap which gave undefined order for equal timestamps,
+  // causing race conditions on multi-core systems (ESP32, BK7200).
+  // With the defer queue:
+  // - Deferred items (delay=0) go directly to defer_queue_ in set_timer_common_
+  // - Items execute in exact order they were deferred (FIFO guarantee)
+  // - No deferred items exist in to_add_, so processing order doesn't affect correctness
+  // ESP8266 and RP2040 don't use this queue - they fall back to the heap-based approach
+  // (ESP8266: single-core, RP2040: empty mutex implementation).
+  while (!this->defer_queue_.empty()) {
+    // The outer check is done without a lock for performance. If the queue
+    // appears non-empty, we lock and process an item. We don't need to check
+    // empty() again inside the lock because only this thread can remove items.
+    std::unique_ptr item;
+    {
+      LockGuard lock(this->lock_);
+      item = std::move(this->defer_queue_.front());
+      this->defer_queue_.pop_front();
+    }
+
+    // Execute callback without holding lock to prevent deadlocks
+    // if the callback tries to call defer() again
+    if (!this->should_skip_item_(item.get())) {
+      this->execute_item_(item.get());
+    }
+  }
+#endif
+
   const auto now = this->millis_();
   this->process_to_add();
 
@@ -282,8 +322,6 @@ void HOT Scheduler::call() {
         this->pop_raw_();
         continue;
       }
-      App.set_current_component(item->component);
-
 #ifdef ESPHOME_DEBUG_SCHEDULER
       const char *item_name = item->get_name();
       ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")",
@@ -294,13 +332,7 @@ void HOT Scheduler::call() {
       // Warning: During callback(), a lot of stuff can happen, including:
       //  - timeouts/intervals get added, potentially invalidating vector pointers
       //  - timeouts/intervals get cancelled
-      {
-        uint32_t now_ms = millis();
-        WarnIfComponentBlockingGuard guard{item->component, now_ms};
-        item->callback();
-        // Call finish to ensure blocking time is properly calculated and reported
-        guard.finish();
-      }
+      this->execute_item_(item.get());
     }
 
     {
@@ -364,6 +396,26 @@ void HOT Scheduler::push_(std::unique_ptr item) {
   LockGuard guard{this->lock_};
   this->to_add_.push_back(std::move(item));
 }
+// Helper function to check if item matches criteria for cancellation
+bool HOT Scheduler::matches_item_(const std::unique_ptr &item, Component *component,
+                                  const char *name_cstr, SchedulerItem::Type type) {
+  if (item->component != component || item->type != type || item->remove) {
+    return false;
+  }
+  const char *item_name = item->get_name();
+  return item_name != nullptr && strcmp(name_cstr, item_name) == 0;
+}
+
+// Helper to execute a scheduler item
+void HOT Scheduler::execute_item_(SchedulerItem *item) {
+  App.set_current_component(item->component);
+
+  uint32_t now_ms = millis();
+  WarnIfComponentBlockingGuard guard{item->component, now_ms};
+  item->callback();
+  guard.finish();
+}
+
 // Common implementation for cancel operations
 bool HOT Scheduler::cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr,
                                         SchedulerItem::Type type) {
@@ -379,19 +431,28 @@ bool HOT Scheduler::cancel_item_common_(Component *component, bool is_static_str
   LockGuard guard{this->lock_};
   bool ret = false;
 
-  for (auto &it : this->items_) {
-    const char *item_name = it->get_name();
-    if (it->component == component && item_name != nullptr && strcmp(name_cstr, item_name) == 0 && it->type == type &&
-        !it->remove) {
-      to_remove_++;
-      it->remove = true;
+  // Check all containers for matching items
+#if !defined(USE_ESP8266) && !defined(USE_RP2040)
+  // Only check defer_queue_ on platforms that have it
+  for (auto &item : this->defer_queue_) {
+    if (this->matches_item_(item, component, name_cstr, type)) {
+      item->remove = true;
       ret = true;
     }
   }
-  for (auto &it : this->to_add_) {
-    const char *item_name = it->get_name();
-    if (it->component == component && item_name != nullptr && strcmp(name_cstr, item_name) == 0 && it->type == type) {
-      it->remove = true;
+#endif
+
+  for (auto &item : this->items_) {
+    if (this->matches_item_(item, component, name_cstr, type)) {
+      item->remove = true;
+      ret = true;
+      this->to_remove_++;  // Only track removals for heap items
+    }
+  }
+
+  for (auto &item : this->to_add_) {
+    if (this->matches_item_(item, component, name_cstr, type)) {
+      item->remove = true;
       ret = true;
     }
   }
diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h
index a64968932e..bf5e63cccf 100644
--- a/esphome/core/scheduler.h
+++ b/esphome/core/scheduler.h
@@ -2,6 +2,7 @@
 
 #include 
 #include 
+#include 
 
 #include "esphome/core/component.h"
 #include "esphome/core/helpers.h"
@@ -142,9 +143,22 @@ class Scheduler {
   // Common implementation for cancel operations
   bool cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type);
 
+ private:
   bool cancel_item_(Component *component, const std::string &name, SchedulerItem::Type type);
   bool cancel_item_(Component *component, const char *name, SchedulerItem::Type type);
 
+  // Helper functions for cancel operations
+  bool matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr,
+                     SchedulerItem::Type type);
+
+  // Helper to execute a scheduler item
+  void execute_item_(SchedulerItem *item);
+
+  // Helper to check if item should be skipped
+  bool should_skip_item_(const SchedulerItem *item) const {
+    return item->remove || (item->component != nullptr && item->component->is_failed());
+  }
+
   bool empty_() {
     this->cleanup_();
     return this->items_.empty();
@@ -153,6 +167,13 @@ class Scheduler {
   Mutex lock_;
   std::vector> items_;
   std::vector> to_add_;
+#if !defined(USE_ESP8266) && !defined(USE_RP2040)
+  // ESP8266 and RP2040 don't need the defer queue because:
+  // ESP8266: Single-core with no preemptive multitasking
+  // RP2040: Currently has empty mutex implementation in ESPHome
+  // Both platforms save 40 bytes of RAM by excluding this
+  std::deque> defer_queue_;  // FIFO queue for defer() calls
+#endif
   uint32_t last_millis_{0};
   uint16_t millis_major_{0};
   uint32_t to_remove_{0};
diff --git a/tests/integration/fixtures/defer_fifo_simple.yaml b/tests/integration/fixtures/defer_fifo_simple.yaml
new file mode 100644
index 0000000000..db24ebf601
--- /dev/null
+++ b/tests/integration/fixtures/defer_fifo_simple.yaml
@@ -0,0 +1,109 @@
+esphome:
+  name: defer-fifo-simple
+
+host:
+
+logger:
+  level: DEBUG
+
+api:
+  services:
+    - service: test_set_timeout
+      then:
+        - lambda: |-
+            // Test set_timeout with 0 delay (direct scheduler call)
+            static int set_timeout_order = 0;
+            static bool set_timeout_passed = true;
+
+            // Reset for this test
+            set_timeout_order = 0;
+            set_timeout_passed = true;
+
+            ESP_LOGD("defer_test", "Testing set_timeout(0) for FIFO order...");
+            for (int i = 0; i < 10; i++) {
+              int expected = i;
+              App.scheduler.set_timeout((Component*)nullptr, nullptr, 0, [expected]() {
+                ESP_LOGD("defer_test", "set_timeout(0) item %d executed, order %d", expected, set_timeout_order);
+                if (set_timeout_order != expected) {
+                  ESP_LOGE("defer_test", "FIFO violation in set_timeout: expected %d but got execution order %d", expected, set_timeout_order);
+                  set_timeout_passed = false;
+                }
+                set_timeout_order++;
+
+                if (set_timeout_order == 10) {
+                  if (set_timeout_passed) {
+                    ESP_LOGI("defer_test", "✓ Test PASSED - set_timeout(0) maintains FIFO order");
+                    id(test_result)->trigger("passed");
+                  } else {
+                    ESP_LOGE("defer_test", "✗ Test FAILED - set_timeout(0) executed out of order");
+                    id(test_result)->trigger("failed");
+                  }
+                  id(test_complete)->trigger("test_finished");
+                }
+              });
+            }
+
+            ESP_LOGD("defer_test", "Deferred 10 items using set_timeout(0), waiting for execution...");
+
+    - service: test_defer
+      then:
+        - lambda: |-
+            // Test defer() method (component method)
+            static int defer_order = 0;
+            static bool defer_passed = true;
+
+            // Reset for this test
+            defer_order = 0;
+            defer_passed = true;
+
+            ESP_LOGD("defer_test", "Testing defer() for FIFO order...");
+
+            // Create a test component class that exposes defer()
+            class TestComponent : public Component {
+            public:
+              void test_defer() {
+                for (int i = 0; i < 10; i++) {
+                  int expected = i;
+                  this->defer([expected]() {
+                    ESP_LOGD("defer_test", "defer() item %d executed, order %d", expected, defer_order);
+                    if (defer_order != expected) {
+                      ESP_LOGE("defer_test", "FIFO violation in defer: expected %d but got execution order %d", expected, defer_order);
+                      defer_passed = false;
+                    }
+                    defer_order++;
+
+                    if (defer_order == 10) {
+                      if (defer_passed) {
+                        ESP_LOGI("defer_test", "✓ Test PASSED - defer() maintains FIFO order");
+                        id(test_result)->trigger("passed");
+                      } else {
+                        ESP_LOGE("defer_test", "✗ Test FAILED - defer() executed out of order");
+                        id(test_result)->trigger("failed");
+                      }
+                      id(test_complete)->trigger("test_finished");
+                    }
+                  });
+                }
+              }
+            };
+
+            // Use a static instance so it doesn't go out of scope
+            static TestComponent test_component;
+            test_component.test_defer();
+
+            ESP_LOGD("defer_test", "Deferred 10 items using defer(), waiting for execution...");
+
+event:
+  - platform: template
+    name: "Test Complete"
+    id: test_complete
+    device_class: button
+    event_types:
+      - "test_finished"
+  - platform: template
+    name: "Test Result"
+    id: test_result
+    device_class: button
+    event_types:
+      - "passed"
+      - "failed"
diff --git a/tests/integration/fixtures/defer_stress.yaml b/tests/integration/fixtures/defer_stress.yaml
new file mode 100644
index 0000000000..6df475229b
--- /dev/null
+++ b/tests/integration/fixtures/defer_stress.yaml
@@ -0,0 +1,38 @@
+esphome:
+  name: defer-stress-test
+
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH
+    components: [defer_stress_component]
+
+host:
+
+logger:
+  level: VERBOSE
+
+defer_stress_component:
+  id: defer_stress
+
+api:
+  services:
+    - service: run_stress_test
+      then:
+        - lambda: |-
+            id(defer_stress)->run_multi_thread_test();
+
+event:
+  - platform: template
+    name: "Test Complete"
+    id: test_complete
+    device_class: button
+    event_types:
+      - "test_finished"
+  - platform: template
+    name: "Test Result"
+    id: test_result
+    device_class: button
+    event_types:
+      - "passed"
+      - "failed"
diff --git a/tests/integration/fixtures/external_components/defer_stress_component/__init__.py b/tests/integration/fixtures/external_components/defer_stress_component/__init__.py
new file mode 100644
index 0000000000..177e595f51
--- /dev/null
+++ b/tests/integration/fixtures/external_components/defer_stress_component/__init__.py
@@ -0,0 +1,19 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+defer_stress_component_ns = cg.esphome_ns.namespace("defer_stress_component")
+DeferStressComponent = defer_stress_component_ns.class_(
+    "DeferStressComponent", cg.Component
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(DeferStressComponent),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
diff --git a/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.cpp b/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.cpp
new file mode 100644
index 0000000000..21ca45947e
--- /dev/null
+++ b/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.cpp
@@ -0,0 +1,75 @@
+#include "defer_stress_component.h"
+#include "esphome/core/log.h"
+#include 
+#include 
+#include 
+#include 
+
+namespace esphome {
+namespace defer_stress_component {
+
+static const char *const TAG = "defer_stress";
+
+void DeferStressComponent::setup() { ESP_LOGCONFIG(TAG, "DeferStressComponent setup"); }
+
+void DeferStressComponent::run_multi_thread_test() {
+  // Use member variables instead of static to avoid issues
+  this->total_defers_ = 0;
+  this->executed_defers_ = 0;
+  static constexpr int NUM_THREADS = 10;
+  static constexpr int DEFERS_PER_THREAD = 100;
+
+  ESP_LOGI(TAG, "Starting defer stress test - multi-threaded concurrent defers");
+
+  // Ensure we're starting clean
+  ESP_LOGI(TAG, "Initial counters: total=%d, executed=%d", this->total_defers_.load(), this->executed_defers_.load());
+
+  // Track start time
+  auto start_time = std::chrono::steady_clock::now();
+
+  // Create threads
+  std::vector threads;
+
+  ESP_LOGI(TAG, "Creating %d threads, each will defer %d callbacks", NUM_THREADS, DEFERS_PER_THREAD);
+
+  threads.reserve(NUM_THREADS);
+  for (int i = 0; i < NUM_THREADS; i++) {
+    threads.emplace_back([this, i]() {
+      ESP_LOGV(TAG, "Thread %d starting", i);
+      // Each thread directly calls defer() without any locking
+      for (int j = 0; j < DEFERS_PER_THREAD; j++) {
+        int defer_id = this->total_defers_.fetch_add(1);
+        ESP_LOGV(TAG, "Thread %d calling defer for request %d", i, defer_id);
+
+        // Capture this pointer safely for the lambda
+        auto *component = this;
+
+        // Directly call defer() from this thread - no locking!
+        this->defer([component, i, j, defer_id]() {
+          component->executed_defers_.fetch_add(1);
+          ESP_LOGV(TAG, "Executed defer %d (thread %d, index %d)", defer_id, i, j);
+        });
+
+        ESP_LOGV(TAG, "Thread %d called defer for request %d successfully", i, defer_id);
+
+        // Small random delay to increase contention
+        if (j % 10 == 0) {
+          std::this_thread::sleep_for(std::chrono::microseconds(100));
+        }
+      }
+      ESP_LOGV(TAG, "Thread %d finished", i);
+    });
+  }
+
+  // Wait for all threads to complete
+  for (auto &t : threads) {
+    t.join();
+  }
+
+  auto end_time = std::chrono::steady_clock::now();
+  auto thread_time = std::chrono::duration_cast(end_time - start_time).count();
+  ESP_LOGI(TAG, "All threads finished in %lldms. Created %d defer requests", thread_time, this->total_defers_.load());
+}
+
+}  // namespace defer_stress_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.h b/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.h
new file mode 100644
index 0000000000..59b7565726
--- /dev/null
+++ b/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include 
+
+namespace esphome {
+namespace defer_stress_component {
+
+class DeferStressComponent : public Component {
+ public:
+  void setup() override;
+  void run_multi_thread_test();
+
+ private:
+  std::atomic total_defers_{0};
+  std::atomic executed_defers_{0};
+};
+
+}  // namespace defer_stress_component
+}  // namespace esphome
diff --git a/tests/integration/test_defer_fifo_simple.py b/tests/integration/test_defer_fifo_simple.py
new file mode 100644
index 0000000000..5a62a45786
--- /dev/null
+++ b/tests/integration/test_defer_fifo_simple.py
@@ -0,0 +1,117 @@
+"""Simple test that defer() maintains FIFO order."""
+
+import asyncio
+
+from aioesphomeapi import EntityState, Event, EventInfo, UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_defer_fifo_simple(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that defer() maintains FIFO order with a simple test."""
+
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "defer-fifo-simple"
+
+        # List entities and services
+        entity_info, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test entities
+        test_complete_entity: EventInfo | None = None
+        test_result_entity: EventInfo | None = None
+
+        for entity in entity_info:
+            if isinstance(entity, EventInfo):
+                if entity.object_id == "test_complete":
+                    test_complete_entity = entity
+                elif entity.object_id == "test_result":
+                    test_result_entity = entity
+
+        assert test_complete_entity is not None, "test_complete event not found"
+        assert test_result_entity is not None, "test_result event not found"
+
+        # Find our test services
+        test_set_timeout_service: UserService | None = None
+        test_defer_service: UserService | None = None
+        for service in services:
+            if service.name == "test_set_timeout":
+                test_set_timeout_service = service
+            elif service.name == "test_defer":
+                test_defer_service = service
+
+        assert test_set_timeout_service is not None, (
+            "test_set_timeout service not found"
+        )
+        assert test_defer_service is not None, "test_defer service not found"
+
+        # Get the event loop
+        loop = asyncio.get_running_loop()
+
+        # Subscribe to states
+        # (events are delivered as EventStates through subscribe_states)
+        test_complete_future: asyncio.Future[bool] = loop.create_future()
+        test_result_future: asyncio.Future[bool] = loop.create_future()
+
+        def on_state(state: EntityState) -> None:
+            if not isinstance(state, Event):
+                return
+
+            if (
+                state.key == test_complete_entity.key
+                and state.event_type == "test_finished"
+                and not test_complete_future.done()
+            ):
+                test_complete_future.set_result(True)
+                return
+
+            if state.key == test_result_entity.key and not test_result_future.done():
+                if state.event_type == "passed":
+                    test_result_future.set_result(True)
+                elif state.event_type == "failed":
+                    test_result_future.set_result(False)
+
+        client.subscribe_states(on_state)
+
+        # Test 1: Test set_timeout(0)
+        client.execute_service(test_set_timeout_service, {})
+
+        # Wait for first test completion
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=5.0)
+            test1_passed = await asyncio.wait_for(test_result_future, timeout=1.0)
+        except asyncio.TimeoutError:
+            pytest.fail("Test set_timeout(0) did not complete within 5 seconds")
+
+        assert test1_passed is True, (
+            "set_timeout(0) FIFO test failed - items executed out of order"
+        )
+
+        # Reset futures for second test
+        test_complete_future = loop.create_future()
+        test_result_future = loop.create_future()
+
+        # Test 2: Test defer()
+        client.execute_service(test_defer_service, {})
+
+        # Wait for second test completion
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=5.0)
+            test2_passed = await asyncio.wait_for(test_result_future, timeout=1.0)
+        except asyncio.TimeoutError:
+            pytest.fail("Test defer() did not complete within 5 seconds")
+
+        # Verify the test passed
+        assert test2_passed is True, (
+            "defer() FIFO test failed - items executed out of order"
+        )
diff --git a/tests/integration/test_defer_stress.py b/tests/integration/test_defer_stress.py
new file mode 100644
index 0000000000..f63ec8d25f
--- /dev/null
+++ b/tests/integration/test_defer_stress.py
@@ -0,0 +1,137 @@
+"""Stress test for defer() thread safety with multiple threads."""
+
+import asyncio
+from pathlib import Path
+import re
+
+from aioesphomeapi import UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_defer_stress(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that defer() doesn't crash when called rapidly from multiple threads."""
+
+    # Get the absolute path to the external components directory
+    external_components_path = str(
+        Path(__file__).parent / "fixtures" / "external_components"
+    )
+
+    # Replace the placeholder in the YAML config with the actual path
+    yaml_config = yaml_config.replace(
+        "EXTERNAL_COMPONENT_PATH", external_components_path
+    )
+
+    # Create a future to signal test completion
+    loop = asyncio.get_event_loop()
+    test_complete_future: asyncio.Future[None] = loop.create_future()
+
+    # Track executed defers and their order
+    executed_defers: set[int] = set()
+    thread_executions: dict[
+        int, list[int]
+    ] = {}  # thread_id -> list of indices in execution order
+    fifo_violations: list[str] = []
+
+    def on_log_line(line: str) -> None:
+        # Track all executed defers with thread and index info
+        match = re.search(r"Executed defer (\d+) \(thread (\d+), index (\d+)\)", line)
+        if not match:
+            return
+
+        defer_id = int(match.group(1))
+        thread_id = int(match.group(2))
+        index = int(match.group(3))
+
+        executed_defers.add(defer_id)
+
+        # Track execution order per thread
+        if thread_id not in thread_executions:
+            thread_executions[thread_id] = []
+
+        # Check FIFO ordering within thread
+        if thread_executions[thread_id] and thread_executions[thread_id][-1] >= index:
+            fifo_violations.append(
+                f"Thread {thread_id}: index {index} executed after "
+                f"{thread_executions[thread_id][-1]}"
+            )
+
+        thread_executions[thread_id].append(index)
+
+        # Check if we've executed all 1000 defers (0-999)
+        if len(executed_defers) == 1000 and not test_complete_future.done():
+            test_complete_future.set_result(None)
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "defer-stress-test"
+
+        # List entities and services
+        entity_info, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test service
+        run_stress_test_service: UserService | None = None
+        for service in services:
+            if service.name == "run_stress_test":
+                run_stress_test_service = service
+                break
+
+        assert run_stress_test_service is not None, "run_stress_test service not found"
+
+        # Call the run_stress_test service to start the test
+        client.execute_service(run_stress_test_service, {})
+
+        # Wait for all defers to execute (should be quick)
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=5.0)
+        except asyncio.TimeoutError:
+            # Report how many we got
+            pytest.fail(
+                f"Stress test timed out. Only {len(executed_defers)} of "
+                f"1000 defers executed. Missing IDs: "
+                f"{sorted(set(range(1000)) - executed_defers)[:10]}..."
+            )
+
+        # Verify all defers executed
+        assert len(executed_defers) == 1000, (
+            f"Expected 1000 defers, got {len(executed_defers)}"
+        )
+
+        # Verify we have all IDs from 0-999
+        expected_ids = set(range(1000))
+        missing_ids = expected_ids - executed_defers
+        assert not missing_ids, f"Missing defer IDs: {sorted(missing_ids)}"
+
+        # Verify FIFO ordering was maintained within each thread
+        assert not fifo_violations, "FIFO ordering violations detected:\n" + "\n".join(
+            fifo_violations[:10]
+        )
+
+        # Verify each thread executed all its defers in order
+        for thread_id, indices in thread_executions.items():
+            assert len(indices) == 100, (
+                f"Thread {thread_id} executed {len(indices)} defers, expected 100"
+            )
+            # Indices should be 0-99 in ascending order
+            assert indices == list(range(100)), (
+                f"Thread {thread_id} executed indices out of order: {indices[:10]}..."
+            )
+
+        # If we got here without crashing and with proper ordering, the test passed
+        assert True, (
+            "Test completed successfully - all 1000 defers executed with "
+            "FIFO ordering preserved"
+        )

From 1368139f4d7aa75c219ca4fa5cab182ed9aa5539 Mon Sep 17 00:00:00 2001
From: Jan-Henrik Bruhn 
Date: Mon, 7 Jul 2025 02:36:09 +0200
Subject: [PATCH 244/293] [update, http_request_update] Implement update
 available trigger (#9174)

---
 .../update/http_request_update.cpp            | 33 +++++++++++++++----
 esphome/components/update/update_entity.h     |  8 +++++
 tests/components/http_request/common.yaml     |  2 ++
 tests/components/update/common.yaml           |  2 ++
 4 files changed, 38 insertions(+), 7 deletions(-)

diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp
index 6bc88ae49a..202c7b88b2 100644
--- a/esphome/components/http_request/update/http_request_update.cpp
+++ b/esphome/components/http_request/update/http_request_update.cpp
@@ -50,7 +50,8 @@ void HttpRequestUpdate::update_task(void *params) {
 
   if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
     std::string msg = str_sprintf("Failed to fetch manifest from %s", this_update->source_url_.c_str());
-    this_update->status_set_error(msg.c_str());
+    // Defer to main loop to avoid race condition on component_state_ read-modify-write
+    this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); });
     UPDATE_RETURN;
   }
 
@@ -58,7 +59,8 @@ void HttpRequestUpdate::update_task(void *params) {
   uint8_t *data = allocator.allocate(container->content_length);
   if (data == nullptr) {
     std::string msg = str_sprintf("Failed to allocate %zu bytes for manifest", container->content_length);
-    this_update->status_set_error(msg.c_str());
+    // Defer to main loop to avoid race condition on component_state_ read-modify-write
+    this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); });
     container->end();
     UPDATE_RETURN;
   }
@@ -120,7 +122,8 @@ void HttpRequestUpdate::update_task(void *params) {
 
   if (!valid) {
     std::string msg = str_sprintf("Failed to parse JSON from %s", this_update->source_url_.c_str());
-    this_update->status_set_error(msg.c_str());
+    // Defer to main loop to avoid race condition on component_state_ read-modify-write
+    this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); });
     UPDATE_RETURN;
   }
 
@@ -147,18 +150,34 @@ void HttpRequestUpdate::update_task(void *params) {
     this_update->update_info_.current_version = current_version;
   }
 
+  bool trigger_update_available = false;
+
   if (this_update->update_info_.latest_version.empty() ||
       this_update->update_info_.latest_version == this_update->update_info_.current_version) {
     this_update->state_ = update::UPDATE_STATE_NO_UPDATE;
   } else {
+    if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) {
+      trigger_update_available = true;
+    }
     this_update->state_ = update::UPDATE_STATE_AVAILABLE;
   }
 
-  this_update->update_info_.has_progress = false;
-  this_update->update_info_.progress = 0.0f;
+  // Defer to main loop to ensure thread-safe execution of:
+  // - status_clear_error() performs non-atomic read-modify-write on component_state_
+  // - publish_state() triggers API callbacks that write to the shared protobuf buffer
+  //   which can be corrupted if accessed concurrently from task and main loop threads
+  // - update_available trigger to ensure consistent state when the trigger fires
+  this_update->defer([this_update, trigger_update_available]() {
+    this_update->update_info_.has_progress = false;
+    this_update->update_info_.progress = 0.0f;
 
-  this_update->status_clear_error();
-  this_update->publish_state();
+    this_update->status_clear_error();
+    this_update->publish_state();
+
+    if (trigger_update_available) {
+      this_update->get_update_available_trigger()->trigger(this_update->update_info_);
+    }
+  });
 
   UPDATE_RETURN;
 }
diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h
index 169e580457..9424e80b9f 100644
--- a/esphome/components/update/update_entity.h
+++ b/esphome/components/update/update_entity.h
@@ -1,5 +1,6 @@
 #pragma once
 
+#include 
 #include "esphome/core/automation.h"
 #include "esphome/core/component.h"
 #include "esphome/core/entity_base.h"
@@ -38,12 +39,19 @@ class UpdateEntity : public EntityBase, public EntityBase_DeviceClass {
   const UpdateState &state = state_;
 
   void add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); }
+  Trigger *get_update_available_trigger() {
+    if (!update_available_trigger_) {
+      update_available_trigger_ = std::make_unique>();
+    }
+    return update_available_trigger_.get();
+  }
 
  protected:
   UpdateState state_{UPDATE_STATE_UNKNOWN};
   UpdateInfo update_info_;
 
   CallbackManager state_callback_{};
+  std::unique_ptr> update_available_trigger_{nullptr};
 };
 
 }  // namespace update
diff --git a/tests/components/http_request/common.yaml b/tests/components/http_request/common.yaml
index af4852901f..97961007e2 100644
--- a/tests/components/http_request/common.yaml
+++ b/tests/components/http_request/common.yaml
@@ -91,3 +91,5 @@ update:
     name: OTA Update
     id: ota_update
     source: http://my.ha.net:8123/local/esphome/manifest.json
+    on_update_available:
+      - logger.log: "A new update is available"
diff --git a/tests/components/update/common.yaml b/tests/components/update/common.yaml
index dcb4f42527..45ed110352 100644
--- a/tests/components/update/common.yaml
+++ b/tests/components/update/common.yaml
@@ -26,3 +26,5 @@ update:
   - platform: http_request
     name: Firmware Update
     source: http://example.com/manifest.json
+    on_update_available:
+      - logger.log: "A new update is available"

From 492580edc39ca991a23441464482157368a5c267 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Sun, 6 Jul 2025 21:50:14 -0500
Subject: [PATCH 245/293] Split LockFreeQueue into base and notifying variants
 to reduce memory usage (#9330)

---
 esphome/components/esp32_ble/ble.h           | 24 +++++--
 esphome/components/mqtt/mqtt_backend_esp32.h |  2 +-
 esphome/core/lock_free_queue.h               | 75 ++++++++++++--------
 3 files changed, 65 insertions(+), 36 deletions(-)

diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h
index ce452d65c4..81582eb09a 100644
--- a/esphome/components/esp32_ble/ble.h
+++ b/esphome/components/esp32_ble/ble.h
@@ -51,7 +51,7 @@ enum IoCapability {
   IO_CAP_KBDISP = ESP_IO_CAP_KBDISP,
 };
 
-enum BLEComponentState {
+enum BLEComponentState : uint8_t {
   /** Nothing has been initialized yet. */
   BLE_COMPONENT_STATE_OFF = 0,
   /** BLE should be disabled on next loop. */
@@ -141,21 +141,31 @@ class ESP32BLE : public Component {
  private:
   template friend void enqueue_ble_event(Args... args);
 
+  // Vectors (12 bytes each on 32-bit, naturally aligned to 4 bytes)
   std::vector gap_event_handlers_;
   std::vector gap_scan_event_handlers_;
   std::vector gattc_event_handlers_;
   std::vector gatts_event_handlers_;
   std::vector ble_status_event_handlers_;
-  BLEComponentState state_{BLE_COMPONENT_STATE_OFF};
 
+  // Large objects (size depends on template parameters, but typically aligned to 4 bytes)
   esphome::LockFreeQueue ble_events_;
   esphome::EventPool ble_event_pool_;
-  BLEAdvertising *advertising_{};
-  esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
-  uint32_t advertising_cycle_time_{};
-  bool enable_on_boot_{};
+
+  // optional (typically 16+ bytes on 32-bit, aligned to 4 bytes)
   optional name_;
-  uint16_t appearance_{0};
+
+  // 4-byte aligned members
+  BLEAdvertising *advertising_{};             // 4 bytes (pointer)
+  esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};  // 4 bytes (enum)
+  uint32_t advertising_cycle_time_{};         // 4 bytes
+
+  // 2-byte aligned members
+  uint16_t appearance_{0};  // 2 bytes
+
+  // 1-byte aligned members (grouped together to minimize padding)
+  BLEComponentState state_{BLE_COMPONENT_STATE_OFF};  // 1 byte (uint8_t enum)
+  bool enable_on_boot_{};                             // 1 byte
 };
 
 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h
index 3611caf554..a24e75eaf9 100644
--- a/esphome/components/mqtt/mqtt_backend_esp32.h
+++ b/esphome/components/mqtt/mqtt_backend_esp32.h
@@ -252,7 +252,7 @@ class MQTTBackendESP32 final : public MQTTBackend {
 #if defined(USE_MQTT_IDF_ENQUEUE)
   static void esphome_mqtt_task(void *params);
   EventPool mqtt_event_pool_;
-  LockFreeQueue mqtt_queue_;
+  NotifyingLockFreeQueue mqtt_queue_;
   TaskHandle_t task_handle_{nullptr};
   bool enqueue_(MqttQueueTypeT type, const char *topic, int qos = 0, bool retain = false, const char *payload = NULL,
                 size_t len = 0);
diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h
index 5460be0fae..f35cfa5af9 100644
--- a/esphome/core/lock_free_queue.h
+++ b/esphome/core/lock_free_queue.h
@@ -31,11 +31,20 @@
 
 namespace esphome {
 
+// Base lock-free queue without task notification
 template class LockFreeQueue {
  public:
-  LockFreeQueue() : head_(0), tail_(0), dropped_count_(0), task_to_notify_(nullptr) {}
+  LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {}
 
   bool push(T *element) {
+    bool was_empty;
+    uint8_t old_tail;
+    return push_internal_(element, was_empty, old_tail);
+  }
+
+ protected:
+  // Internal push that reports queue state - for use by derived classes
+  bool push_internal_(T *element, bool &was_empty, uint8_t &old_tail) {
     if (element == nullptr)
       return false;
 
@@ -51,34 +60,16 @@ template class LockFreeQueue {
       return false;
     }
 
-    // Check if queue was empty before push
-    bool was_empty = (current_tail == head_before);
+    was_empty = (current_tail == head_before);
+    old_tail = current_tail;
 
     buffer_[current_tail] = element;
     tail_.store(next_tail, std::memory_order_release);
 
-    // Notify optimization: only notify if we need to
-    if (task_to_notify_ != nullptr) {
-      if (was_empty) {
-        // Queue was empty - consumer might be going to sleep, must notify
-        xTaskNotifyGive(task_to_notify_);
-      } else {
-        // Queue wasn't empty - check if consumer has caught up to previous tail
-        uint8_t head_after = head_.load(std::memory_order_acquire);
-        if (head_after == current_tail) {
-          // Consumer just caught up to where tail was - might go to sleep, must notify
-          // Note: There's a benign race here - between reading head_after and calling
-          // xTaskNotifyGive(), the consumer could advance further. This would result
-          // in an unnecessary wake-up, but is harmless and extremely rare in practice.
-          xTaskNotifyGive(task_to_notify_);
-        }
-        // Otherwise: consumer is still behind, no need to notify
-      }
-    }
-
     return true;
   }
 
+ public:
   T *pop() {
     uint8_t current_head = head_.load(std::memory_order_relaxed);
 
@@ -108,11 +99,6 @@ template class LockFreeQueue {
     return next_tail == head_.load(std::memory_order_acquire);
   }
 
-  // Set the FreeRTOS task handle to notify when items are pushed to the queue
-  // This enables efficient wake-up of a consumer task that's waiting for data
-  // @param task The FreeRTOS task handle to notify, or nullptr to disable notifications
-  void set_task_to_notify(TaskHandle_t task) { task_to_notify_ = task; }
-
  protected:
   T *buffer_[SIZE];
   // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset)
@@ -123,7 +109,40 @@ template class LockFreeQueue {
   std::atomic head_;
   // Atomic: written by producer (push), read by consumer (pop) to check if empty
   std::atomic tail_;
-  // Task handle for notification (optional)
+};
+
+// Extended queue with task notification support
+template class NotifyingLockFreeQueue : public LockFreeQueue {
+ public:
+  NotifyingLockFreeQueue() : LockFreeQueue(), task_to_notify_(nullptr) {}
+
+  bool push(T *element) {
+    bool was_empty;
+    uint8_t old_tail;
+    bool result = this->push_internal_(element, was_empty, old_tail);
+
+    // Notify optimization: only notify if we need to
+    if (result && task_to_notify_ != nullptr &&
+        (was_empty || this->head_.load(std::memory_order_acquire) == old_tail)) {
+      // Notify in two cases:
+      // 1. Queue was empty - consumer might be going to sleep
+      // 2. Consumer just caught up to where tail was - might go to sleep
+      // Note: There's a benign race in case 2 - between reading head and calling
+      // xTaskNotifyGive(), the consumer could advance further. This would result
+      // in an unnecessary wake-up, but is harmless and extremely rare in practice.
+      xTaskNotifyGive(task_to_notify_);
+    }
+    // Otherwise: consumer is still behind, no need to notify
+
+    return result;
+  }
+
+  // Set the FreeRTOS task handle to notify when items are pushed to the queue
+  // This enables efficient wake-up of a consumer task that's waiting for data
+  // @param task The FreeRTOS task handle to notify, or nullptr to disable notifications
+  void set_task_to_notify(TaskHandle_t task) { task_to_notify_ = task; }
+
+ private:
   TaskHandle_t task_to_notify_;
 };
 

From a303f9323682911c91a8c3eed50ec7dd1d6ba8d8 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Sun, 6 Jul 2025 21:50:36 -0500
Subject: [PATCH 246/293] Fix bluetooth proxy busy loop when disconnecting
 pending BLE connections (#9332)

---
 esphome/components/bluetooth_proxy/bluetooth_proxy.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp
index fbe2a3e67c..bf0adf1efd 100644
--- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp
+++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp
@@ -170,7 +170,7 @@ int BluetoothProxy::get_bluetooth_connections_free() {
 void BluetoothProxy::loop() {
   if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) {
     for (auto *connection : this->connections_) {
-      if (connection->get_address() != 0) {
+      if (connection->get_address() != 0 && !connection->disconnect_pending()) {
         connection->disconnect();
       }
     }

From 765793505d64d01162abaa5bbaeb7f0ee4f9d974 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Sun, 6 Jul 2025 21:53:23 -0500
Subject: [PATCH 247/293] Use std::span to eliminate heap allocation for
 single-packet API transmissions (#9313)

---
 esphome/components/api/api_frame_helper.cpp | 86 ++++++++-------------
 esphome/components/api/api_frame_helper.h   |  7 +-
 2 files changed, 36 insertions(+), 57 deletions(-)

diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp
index af6dd0220d..6ed9c95354 100644
--- a/esphome/components/api/api_frame_helper.cpp
+++ b/esphome/components/api/api_frame_helper.cpp
@@ -614,20 +614,14 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
   return APIError::OK;
 }
 APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
-  std::vector *raw_buffer = buffer.get_buffer();
-  uint16_t payload_len = static_cast(raw_buffer->size() - frame_header_padding_);
-
   // Resize to include MAC space (required for Noise encryption)
-  raw_buffer->resize(raw_buffer->size() + frame_footer_size_);
-
-  // Use write_protobuf_packets with a single packet
-  std::vector packets;
-  packets.emplace_back(type, 0, payload_len);
-
-  return write_protobuf_packets(buffer, packets);
+  buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
+  PacketInfo packet{type, 0,
+                    static_cast(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
+  return write_protobuf_packets(buffer, std::span(&packet, 1));
 }
 
-APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector &packets) {
+APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) {
   APIError aerr = state_action_();
   if (aerr != APIError::OK) {
     return aerr;
@@ -642,18 +636,15 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, co
   }
 
   std::vector *raw_buffer = buffer.get_buffer();
+  uint8_t *buffer_data = raw_buffer->data();  // Cache buffer pointer
+
   this->reusable_iovs_.clear();
   this->reusable_iovs_.reserve(packets.size());
 
   // We need to encrypt each packet in place
   for (const auto &packet : packets) {
-    uint16_t type = packet.message_type;
-    uint16_t offset = packet.offset;
-    uint16_t payload_len = packet.payload_size;
-    uint16_t msg_len = 4 + payload_len;  // type(2) + data_len(2) + payload
-
     // The buffer already has padding at offset
-    uint8_t *buf_start = raw_buffer->data() + offset;
+    uint8_t *buf_start = buffer_data + packet.offset;
 
     // Write noise header
     buf_start[0] = 0x01;  // indicator
@@ -661,10 +652,10 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, co
 
     // Write message header (to be encrypted)
     const uint8_t msg_offset = 3;
-    buf_start[msg_offset + 0] = (uint8_t) (type >> 8);         // type high byte
-    buf_start[msg_offset + 1] = (uint8_t) type;                // type low byte
-    buf_start[msg_offset + 2] = (uint8_t) (payload_len >> 8);  // data_len high byte
-    buf_start[msg_offset + 3] = (uint8_t) payload_len;         // data_len low byte
+    buf_start[msg_offset] = static_cast(packet.message_type >> 8);      // type high byte
+    buf_start[msg_offset + 1] = static_cast(packet.message_type);       // type low byte
+    buf_start[msg_offset + 2] = static_cast(packet.payload_size >> 8);  // data_len high byte
+    buf_start[msg_offset + 3] = static_cast(packet.payload_size);       // data_len low byte
     // payload data is already in the buffer starting at offset + 7
 
     // Make sure we have space for MAC
@@ -673,7 +664,8 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, co
     // Encrypt the message in place
     NoiseBuffer mbuf;
     noise_buffer_init(mbuf);
-    noise_buffer_set_inout(mbuf, buf_start + msg_offset, msg_len, msg_len + frame_footer_size_);
+    noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + packet.payload_size,
+                           4 + packet.payload_size + frame_footer_size_);
 
     int err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
     if (err != 0) {
@@ -683,14 +675,12 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, co
     }
 
     // Fill in the encrypted size
-    buf_start[1] = (uint8_t) (mbuf.size >> 8);
-    buf_start[2] = (uint8_t) mbuf.size;
+    buf_start[1] = static_cast(mbuf.size >> 8);
+    buf_start[2] = static_cast(mbuf.size);
 
     // Add iovec for this encrypted packet
-    struct iovec iov;
-    iov.iov_base = buf_start;
-    iov.iov_len = 3 + mbuf.size;  // indicator + size + encrypted data
-    this->reusable_iovs_.push_back(iov);
+    this->reusable_iovs_.push_back(
+        {buf_start, static_cast(3 + mbuf.size)});  // indicator + size + encrypted data
   }
 
   // Send all encrypted packets in one writev call
@@ -1029,18 +1019,11 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
   return APIError::OK;
 }
 APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
-  std::vector *raw_buffer = buffer.get_buffer();
-  uint16_t payload_len = static_cast(raw_buffer->size() - frame_header_padding_);
-
-  // Use write_protobuf_packets with a single packet
-  std::vector packets;
-  packets.emplace_back(type, 0, payload_len);
-
-  return write_protobuf_packets(buffer, packets);
+  PacketInfo packet{type, 0, static_cast(buffer.get_buffer()->size() - frame_header_padding_)};
+  return write_protobuf_packets(buffer, std::span(&packet, 1));
 }
 
-APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer,
-                                                         const std::vector &packets) {
+APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) {
   if (state_ != State::DATA) {
     return APIError::BAD_STATE;
   }
@@ -1050,17 +1033,15 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
   }
 
   std::vector *raw_buffer = buffer.get_buffer();
+  uint8_t *buffer_data = raw_buffer->data();  // Cache buffer pointer
+
   this->reusable_iovs_.clear();
   this->reusable_iovs_.reserve(packets.size());
 
   for (const auto &packet : packets) {
-    uint16_t type = packet.message_type;
-    uint16_t offset = packet.offset;
-    uint16_t payload_len = packet.payload_size;
-
     // Calculate varint sizes for header layout
-    uint8_t size_varint_len = api::ProtoSize::varint(static_cast(payload_len));
-    uint8_t type_varint_len = api::ProtoSize::varint(static_cast(type));
+    uint8_t size_varint_len = api::ProtoSize::varint(static_cast(packet.payload_size));
+    uint8_t type_varint_len = api::ProtoSize::varint(static_cast(packet.message_type));
     uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
 
     // Calculate where to start writing the header
@@ -1088,23 +1069,20 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
     //
     // The message starts at offset + frame_header_padding_
     // So we write the header starting at offset + frame_header_padding_ - total_header_len
-    uint8_t *buf_start = raw_buffer->data() + offset;
+    uint8_t *buf_start = buffer_data + packet.offset;
     uint32_t header_offset = frame_header_padding_ - total_header_len;
 
     // Write the plaintext header
     buf_start[header_offset] = 0x00;  // indicator
 
-    // Encode size varint directly into buffer
-    ProtoVarInt(payload_len).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
-
-    // Encode type varint directly into buffer
-    ProtoVarInt(type).encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
+    // Encode varints directly into buffer
+    ProtoVarInt(packet.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
+    ProtoVarInt(packet.message_type)
+        .encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
 
     // Add iovec for this packet (header + payload)
-    struct iovec iov;
-    iov.iov_base = buf_start + header_offset;
-    iov.iov_len = total_header_len + payload_len;
-    this->reusable_iovs_.push_back(iov);
+    this->reusable_iovs_.push_back(
+        {buf_start + header_offset, static_cast(total_header_len + packet.payload_size)});
   }
 
   // Send all packets in one writev call
diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h
index 1e157278a1..1bb6bc7ed3 100644
--- a/esphome/components/api/api_frame_helper.h
+++ b/esphome/components/api/api_frame_helper.h
@@ -2,6 +2,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
@@ -101,7 +102,7 @@ class APIFrameHelper {
   // Write multiple protobuf packets in a single operation
   // packets contains (message_type, offset, length) for each message in the buffer
   // The buffer contains all messages with appropriate padding before each
-  virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector &packets) = 0;
+  virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) = 0;
   // Get the frame header padding required by this protocol
   virtual uint8_t frame_header_padding() = 0;
   // Get the frame footer size required by this protocol
@@ -194,7 +195,7 @@ class APINoiseFrameHelper : public APIFrameHelper {
   APIError loop() override;
   APIError read_packet(ReadPacketBuffer *buffer) override;
   APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
-  APIError write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector &packets) override;
+  APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override;
   // Get the frame header padding required by this protocol
   uint8_t frame_header_padding() override { return frame_header_padding_; }
   // Get the frame footer size required by this protocol
@@ -248,7 +249,7 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
   APIError loop() override;
   APIError read_packet(ReadPacketBuffer *buffer) override;
   APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
-  APIError write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector &packets) override;
+  APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override;
   uint8_t frame_header_padding() override { return frame_header_padding_; }
   // Get the frame footer size required by this protocol
   uint8_t frame_footer_size() override { return frame_footer_size_; }

From bdd52dbaa41593dbb74ea087193a6c3e38169899 Mon Sep 17 00:00:00 2001
From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Date: Sun, 6 Jul 2025 23:41:47 -0400
Subject: [PATCH 248/293] [sx127x] Fix shaping print in dump_config and
 preallocate packet (#9357)

---
 esphome/components/sx127x/sx127x.cpp | 51 +++++++++++++++-------------
 esphome/components/sx127x/sx127x.h   |  1 +
 2 files changed, 28 insertions(+), 24 deletions(-)

diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp
index 7f62ee2bd3..2d2326549b 100644
--- a/esphome/components/sx127x/sx127x.cpp
+++ b/esphome/components/sx127x/sx127x.cpp
@@ -318,24 +318,23 @@ void SX127x::loop() {
       uint8_t addr = this->read_register_(REG_FIFO_RX_CURR_ADDR);
       uint8_t rssi = this->read_register_(REG_PKT_RSSI_VALUE);
       int8_t snr = (int8_t) this->read_register_(REG_PKT_SNR_VALUE);
-      std::vector packet(bytes);
+      this->packet_.resize(bytes);
       this->write_register_(REG_FIFO_ADDR_PTR, addr);
-      this->read_fifo_(packet);
+      this->read_fifo_(this->packet_);
       if (this->frequency_ > 700000000) {
-        this->call_listeners_(packet, (float) rssi - RSSI_OFFSET_HF, (float) snr / 4);
+        this->call_listeners_(this->packet_, (float) rssi - RSSI_OFFSET_HF, (float) snr / 4);
       } else {
-        this->call_listeners_(packet, (float) rssi - RSSI_OFFSET_LF, (float) snr / 4);
+        this->call_listeners_(this->packet_, (float) rssi - RSSI_OFFSET_LF, (float) snr / 4);
       }
     }
   } else if (this->packet_mode_) {
-    std::vector packet;
     uint8_t payload_length = this->payload_length_;
     if (payload_length == 0) {
       payload_length = this->read_register_(REG_FIFO);
     }
-    packet.resize(payload_length);
-    this->read_fifo_(packet);
-    this->call_listeners_(packet, 0.0f, 0.0f);
+    this->packet_.resize(payload_length);
+    this->read_fifo_(this->packet_);
+    this->call_listeners_(this->packet_, 0.0f, 0.0f);
   }
 }
 
@@ -407,18 +406,6 @@ void SX127x::dump_config() {
   LOG_PIN("  CS Pin: ", this->cs_);
   LOG_PIN("  RST Pin: ", this->rst_pin_);
   LOG_PIN("  DIO0 Pin: ", this->dio0_pin_);
-  const char *shaping = "NONE";
-  if (this->shaping_ == CUTOFF_BR_X_2) {
-    shaping = "CUTOFF_BR_X_2";
-  } else if (this->shaping_ == CUTOFF_BR_X_1) {
-    shaping = "CUTOFF_BR_X_1";
-  } else if (this->shaping_ == GAUSSIAN_BT_0_3) {
-    shaping = "GAUSSIAN_BT_0_3";
-  } else if (this->shaping_ == GAUSSIAN_BT_0_5) {
-    shaping = "GAUSSIAN_BT_0_5";
-  } else if (this->shaping_ == GAUSSIAN_BT_1_0) {
-    shaping = "GAUSSIAN_BT_1_0";
-  }
   const char *pa_pin = "RFO";
   if (this->pa_pin_ == PA_PIN_BOOST) {
     pa_pin = "BOOST";
@@ -429,10 +416,9 @@ void SX127x::dump_config() {
                 "  Bandwidth: %" PRIu32 " Hz\n"
                 "  PA Pin: %s\n"
                 "  PA Power: %" PRIu8 " dBm\n"
-                "  PA Ramp: %" PRIu16 " us\n"
-                "  Shaping: %s",
+                "  PA Ramp: %" PRIu16 " us",
                 TRUEFALSE(this->auto_cal_), this->frequency_, BW_HZ[this->bandwidth_], pa_pin, this->pa_power_,
-                RAMP[this->pa_ramp_], shaping);
+                RAMP[this->pa_ramp_]);
   if (this->modulation_ == MOD_FSK) {
     ESP_LOGCONFIG(TAG, "  Deviation: %" PRIu32 " Hz", this->deviation_);
   }
@@ -459,14 +445,31 @@ void SX127x::dump_config() {
       ESP_LOGCONFIG(TAG, "  Sync Value: 0x%02x", this->sync_value_[0]);
     }
   } else {
+    const char *shaping = "NONE";
+    if (this->modulation_ == MOD_FSK) {
+      if (this->shaping_ == GAUSSIAN_BT_0_3) {
+        shaping = "GAUSSIAN_BT_0_3";
+      } else if (this->shaping_ == GAUSSIAN_BT_0_5) {
+        shaping = "GAUSSIAN_BT_0_5";
+      } else if (this->shaping_ == GAUSSIAN_BT_1_0) {
+        shaping = "GAUSSIAN_BT_1_0";
+      }
+    } else {
+      if (this->shaping_ == CUTOFF_BR_X_2) {
+        shaping = "CUTOFF_BR_X_2";
+      } else if (this->shaping_ == CUTOFF_BR_X_1) {
+        shaping = "CUTOFF_BR_X_1";
+      }
+    }
     ESP_LOGCONFIG(TAG,
+                  "  Shaping: %s\n"
                   "  Modulation: %s\n"
                   "  Bitrate: %" PRIu32 "b/s\n"
                   "  Bitsync: %s\n"
                   "  Rx Start: %s\n"
                   "  Rx Floor: %.1f dBm\n"
                   "  Packet Mode: %s",
-                  this->modulation_ == MOD_FSK ? "FSK" : "OOK", this->bitrate_, TRUEFALSE(this->bitsync_),
+                  shaping, this->modulation_ == MOD_FSK ? "FSK" : "OOK", this->bitrate_, TRUEFALSE(this->bitsync_),
                   TRUEFALSE(this->rx_start_), this->rx_floor_, TRUEFALSE(this->packet_mode_));
     if (this->packet_mode_) {
       ESP_LOGCONFIG(TAG, "  CRC Enable: %s", TRUEFALSE(this->crc_enable_));
diff --git a/esphome/components/sx127x/sx127x.h b/esphome/components/sx127x/sx127x.h
index 4cc7c9b6d3..0600b51201 100644
--- a/esphome/components/sx127x/sx127x.h
+++ b/esphome/components/sx127x/sx127x.h
@@ -96,6 +96,7 @@ class SX127x : public Component,
   uint8_t read_register_(uint8_t reg);
   Trigger, float, float> *packet_trigger_{new Trigger, float, float>()};
   std::vector listeners_;
+  std::vector packet_;
   std::vector sync_value_;
   InternalGPIOPin *dio0_pin_{nullptr};
   InternalGPIOPin *rst_pin_{nullptr};

From e49b89a0517ea4ff446005da3281fa8434f9af7e Mon Sep 17 00:00:00 2001
From: DT-art1 <81360462+DT-art1@users.noreply.github.com>
Date: Mon, 7 Jul 2025 05:45:00 +0200
Subject: [PATCH 249/293] Introduce base Camera class to support alternative
 camera implementations (#9285)

Co-authored-by: J. Nick Koston 
Co-authored-by: J. Nick Koston 
---
 CODEOWNERS                                    |  1 +
 esphome/components/api/api.proto              |  6 +-
 esphome/components/api/api_connection.cpp     | 51 ++++++------
 esphome/components/api/api_connection.h       | 10 +--
 esphome/components/api/api_pb2.cpp            |  2 +-
 esphome/components/api/api_pb2.h              |  2 +-
 esphome/components/api/api_pb2_dump.cpp       |  2 +-
 esphome/components/api/api_pb2_service.cpp    |  4 +-
 esphome/components/api/api_pb2_service.h      |  6 +-
 esphome/components/api/api_server.cpp         | 17 ++--
 esphome/components/api/list_entities.cpp      |  4 +-
 esphome/components/api/list_entities.h        |  4 +-
 esphome/components/camera/__init__.py         |  1 +
 esphome/components/camera/camera.cpp          | 22 +++++
 esphome/components/camera/camera.h            | 80 +++++++++++++++++++
 esphome/components/esp32_camera/__init__.py   |  3 +-
 .../components/esp32_camera/esp32_camera.cpp  | 53 ++++++------
 .../components/esp32_camera/esp32_camera.h    | 49 +++++-------
 .../esp32_camera_web_server/__init__.py       |  3 +-
 .../camera_web_server.cpp                     | 16 ++--
 .../camera_web_server.h                       |  6 +-
 esphome/core/component_iterator.cpp           | 12 +--
 esphome/core/component_iterator.h             | 10 +--
 esphome/core/defines.h                        |  2 +-
 tests/components/camera/common.yaml           | 18 +++++
 tests/components/camera/test.esp32-ard.yaml   |  1 +
 tests/components/camera/test.esp32-idf.yaml   |  1 +
 27 files changed, 254 insertions(+), 132 deletions(-)
 create mode 100644 esphome/components/camera/__init__.py
 create mode 100644 esphome/components/camera/camera.cpp
 create mode 100644 esphome/components/camera/camera.h
 create mode 100644 tests/components/camera/common.yaml
 create mode 100644 tests/components/camera/test.esp32-ard.yaml
 create mode 100644 tests/components/camera/test.esp32-idf.yaml

diff --git a/CODEOWNERS b/CODEOWNERS
index 540f33853d..1a7dc4f227 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -87,6 +87,7 @@ esphome/components/bp1658cj/* @Cossid
 esphome/components/bp5758d/* @Cossid
 esphome/components/button/* @esphome/core
 esphome/components/bytebuffer/* @clydebarrow
+esphome/components/camera/* @DT-art1 @bdraco
 esphome/components/canbus/* @danielschramm @mvturnho
 esphome/components/cap1188/* @mreditor97
 esphome/components/captive_portal/* @OttoWinter
diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto
index a9aa0b4bff..c3795bb796 100644
--- a/esphome/components/api/api.proto
+++ b/esphome/components/api/api.proto
@@ -836,7 +836,7 @@ message ListEntitiesCameraResponse {
   option (id) = 43;
   option (base_class) = "InfoResponseProtoMessage";
   option (source) = SOURCE_SERVER;
-  option (ifdef) = "USE_ESP32_CAMERA";
+  option (ifdef) = "USE_CAMERA";
 
   string object_id = 1;
   fixed32 key = 2;
@@ -851,7 +851,7 @@ message ListEntitiesCameraResponse {
 message CameraImageResponse {
   option (id) = 44;
   option (source) = SOURCE_SERVER;
-  option (ifdef) = "USE_ESP32_CAMERA";
+  option (ifdef) = "USE_CAMERA";
 
   fixed32 key = 1;
   bytes data = 2;
@@ -860,7 +860,7 @@ message CameraImageResponse {
 message CameraImageRequest {
   option (id) = 45;
   option (source) = SOURCE_CLIENT;
-  option (ifdef) = "USE_ESP32_CAMERA";
+  option (ifdef) = "USE_CAMERA";
   option (no_delay) = true;
 
   bool single = 1;
diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp
index 4d99bdbbd6..51a5769f99 100644
--- a/esphome/components/api/api_connection.cpp
+++ b/esphome/components/api/api_connection.cpp
@@ -38,8 +38,8 @@ static constexpr uint16_t PING_RETRY_INTERVAL = 1000;
 static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2;
 
 static const char *const TAG = "api.connection";
-#ifdef USE_ESP32_CAMERA
-static const int ESP32_CAMERA_STOP_STREAM = 5000;
+#ifdef USE_CAMERA
+static const int CAMERA_STOP_STREAM = 5000;
 #endif
 
 APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent)
@@ -58,6 +58,11 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa
 #else
 #error "No frame helper defined"
 #endif
+#ifdef USE_CAMERA
+  if (camera::Camera::instance() != nullptr) {
+    this->image_reader_ = std::unique_ptr{camera::Camera::instance()->create_image_reader()};
+  }
+#endif
 }
 
 uint32_t APIConnection::get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); }
@@ -180,10 +185,10 @@ void APIConnection::loop() {
     }
   }
 
-#ifdef USE_ESP32_CAMERA
-  if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) {
-    uint32_t to_send = std::min((size_t) MAX_PACKET_SIZE, this->image_reader_.available());
-    bool done = this->image_reader_.available() == to_send;
+#ifdef USE_CAMERA
+  if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) {
+    uint32_t to_send = std::min((size_t) MAX_PACKET_SIZE, this->image_reader_->available());
+    bool done = this->image_reader_->available() == to_send;
     uint32_t msg_size = 0;
     ProtoSize::add_fixed_field<4>(msg_size, 1, true);
     // partial message size calculated manually since its a special case
@@ -193,18 +198,18 @@ void APIConnection::loop() {
 
     auto buffer = this->create_buffer(msg_size);
     // fixed32 key = 1;
-    buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash());
+    buffer.encode_fixed32(1, camera::Camera::instance()->get_object_id_hash());
     // bytes data = 2;
-    buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send);
+    buffer.encode_bytes(2, this->image_reader_->peek_data_buffer(), to_send);
     // bool done = 3;
     buffer.encode_bool(3, done);
 
     bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE);
 
     if (success) {
-      this->image_reader_.consume_data(to_send);
+      this->image_reader_->consume_data(to_send);
       if (done) {
-        this->image_reader_.return_image();
+        this->image_reader_->return_image();
       }
     }
   }
@@ -1112,36 +1117,36 @@ void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
 }
 #endif
 
-#ifdef USE_ESP32_CAMERA
-void APIConnection::set_camera_state(std::shared_ptr image) {
+#ifdef USE_CAMERA
+void APIConnection::set_camera_state(std::shared_ptr image) {
   if (!this->flags_.state_subscription)
     return;
-  if (this->image_reader_.available())
+  if (!this->image_reader_)
     return;
-  if (image->was_requested_by(esphome::esp32_camera::API_REQUESTER) ||
-      image->was_requested_by(esphome::esp32_camera::IDLE))
-    this->image_reader_.set_image(std::move(image));
+  if (this->image_reader_->available())
+    return;
+  if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE))
+    this->image_reader_->set_image(std::move(image));
 }
 uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
                                              bool is_single) {
-  auto *camera = static_cast(entity);
+  auto *camera = static_cast(entity);
   ListEntitiesCameraResponse msg;
   msg.unique_id = get_default_unique_id("camera", camera);
   fill_entity_info_base(camera, msg);
   return encode_message_to_buffer(msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 }
 void APIConnection::camera_image(const CameraImageRequest &msg) {
-  if (esp32_camera::global_esp32_camera == nullptr)
+  if (camera::Camera::instance() == nullptr)
     return;
 
   if (msg.single)
-    esp32_camera::global_esp32_camera->request_image(esphome::esp32_camera::API_REQUESTER);
+    camera::Camera::instance()->request_image(esphome::camera::API_REQUESTER);
   if (msg.stream) {
-    esp32_camera::global_esp32_camera->start_stream(esphome::esp32_camera::API_REQUESTER);
+    camera::Camera::instance()->start_stream(esphome::camera::API_REQUESTER);
 
-    App.scheduler.set_timeout(this->parent_, "api_esp32_camera_stop_stream", ESP32_CAMERA_STOP_STREAM, []() {
-      esp32_camera::global_esp32_camera->stop_stream(esphome::esp32_camera::API_REQUESTER);
-    });
+    App.scheduler.set_timeout(this->parent_, "api_camera_stop_stream", CAMERA_STOP_STREAM,
+                              []() { camera::Camera::instance()->stop_stream(esphome::camera::API_REQUESTER); });
   }
 }
 #endif
diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h
index dc4b84a535..166dbc3656 100644
--- a/esphome/components/api/api_connection.h
+++ b/esphome/components/api/api_connection.h
@@ -60,8 +60,8 @@ class APIConnection : public APIServerConnection {
 #ifdef USE_TEXT_SENSOR
   bool send_text_sensor_state(text_sensor::TextSensor *text_sensor);
 #endif
-#ifdef USE_ESP32_CAMERA
-  void set_camera_state(std::shared_ptr image);
+#ifdef USE_CAMERA
+  void set_camera_state(std::shared_ptr image);
   void camera_image(const CameraImageRequest &msg) override;
 #endif
 #ifdef USE_CLIMATE
@@ -425,7 +425,7 @@ class APIConnection : public APIServerConnection {
   static uint16_t try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
                                        bool is_single);
 #endif
-#ifdef USE_ESP32_CAMERA
+#ifdef USE_CAMERA
   static uint16_t try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
                                        bool is_single);
 #endif
@@ -455,8 +455,8 @@ class APIConnection : public APIServerConnection {
   // These contain vectors/pointers internally, so putting them early ensures good alignment
   InitialStateIterator initial_state_iterator_;
   ListEntitiesIterator list_entities_iterator_;
-#ifdef USE_ESP32_CAMERA
-  esp32_camera::CameraImageReader image_reader_;
+#ifdef USE_CAMERA
+  std::unique_ptr image_reader_;
 #endif
 
   // Group 3: Strings (12 bytes each on 32-bit, 4-byte aligned)
diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp
index 5c2b22d22a..3505ec758d 100644
--- a/esphome/components/api/api_pb2.cpp
+++ b/esphome/components/api/api_pb2.cpp
@@ -2216,7 +2216,7 @@ void ExecuteServiceRequest::calculate_size(uint32_t &total_size) const {
   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
   ProtoSize::add_repeated_message(total_size, 1, this->args);
 }
-#ifdef USE_ESP32_CAMERA
+#ifdef USE_CAMERA
 bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 5: {
diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h
index c0079bd29c..3bfc5f1cf4 100644
--- a/esphome/components/api/api_pb2.h
+++ b/esphome/components/api/api_pb2.h
@@ -1273,7 +1273,7 @@ class ExecuteServiceRequest : public ProtoMessage {
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
 };
-#ifdef USE_ESP32_CAMERA
+#ifdef USE_CAMERA
 class ListEntitiesCameraResponse : public InfoResponseProtoMessage {
  public:
   static constexpr uint16_t MESSAGE_TYPE = 43;
diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp
index db330a17fb..84e765e40f 100644
--- a/esphome/components/api/api_pb2_dump.cpp
+++ b/esphome/components/api/api_pb2_dump.cpp
@@ -1890,7 +1890,7 @@ void ExecuteServiceRequest::dump_to(std::string &out) const {
   }
   out.append("}");
 }
-#ifdef USE_ESP32_CAMERA
+#ifdef USE_CAMERA
 void ListEntitiesCameraResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("ListEntitiesCameraResponse {\n");
diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp
index de8e6574b2..92dd90053b 100644
--- a/esphome/components/api/api_pb2_service.cpp
+++ b/esphome/components/api/api_pb2_service.cpp
@@ -204,7 +204,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
       this->on_execute_service_request(msg);
       break;
     }
-#ifdef USE_ESP32_CAMERA
+#ifdef USE_CAMERA
     case 45: {
       CameraImageRequest msg;
       msg.decode(msg_data, msg_size);
@@ -682,7 +682,7 @@ void APIServerConnection::on_button_command_request(const ButtonCommandRequest &
   }
 }
 #endif
-#ifdef USE_ESP32_CAMERA
+#ifdef USE_CAMERA
 void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) {
   if (this->check_authenticated_()) {
     this->camera_image(msg);
diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h
index 8c870e5e1c..458f8ec81b 100644
--- a/esphome/components/api/api_pb2_service.h
+++ b/esphome/components/api/api_pb2_service.h
@@ -71,7 +71,7 @@ class APIServerConnectionBase : public ProtoService {
 
   virtual void on_execute_service_request(const ExecuteServiceRequest &value){};
 
-#ifdef USE_ESP32_CAMERA
+#ifdef USE_CAMERA
   virtual void on_camera_image_request(const CameraImageRequest &value){};
 #endif
 
@@ -223,7 +223,7 @@ class APIServerConnection : public APIServerConnectionBase {
 #ifdef USE_BUTTON
   virtual void button_command(const ButtonCommandRequest &msg) = 0;
 #endif
-#ifdef USE_ESP32_CAMERA
+#ifdef USE_CAMERA
   virtual void camera_image(const CameraImageRequest &msg) = 0;
 #endif
 #ifdef USE_CLIMATE
@@ -340,7 +340,7 @@ class APIServerConnection : public APIServerConnectionBase {
 #ifdef USE_BUTTON
   void on_button_command_request(const ButtonCommandRequest &msg) override;
 #endif
-#ifdef USE_ESP32_CAMERA
+#ifdef USE_CAMERA
   void on_camera_image_request(const CameraImageRequest &msg) override;
 #endif
 #ifdef USE_CLIMATE
diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp
index 4dc6fe2390..575229cf04 100644
--- a/esphome/components/api/api_server.cpp
+++ b/esphome/components/api/api_server.cpp
@@ -119,15 +119,14 @@ void APIServer::setup() {
   }
 #endif
 
-#ifdef USE_ESP32_CAMERA
-  if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) {
-    esp32_camera::global_esp32_camera->add_image_callback(
-        [this](const std::shared_ptr &image) {
-          for (auto &c : this->clients_) {
-            if (!c->flags_.remove)
-              c->set_camera_state(image);
-          }
-        });
+#ifdef USE_CAMERA
+  if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) {
+    camera::Camera::instance()->add_image_callback([this](const std::shared_ptr &image) {
+      for (auto &c : this->clients_) {
+        if (!c->flags_.remove)
+          c->set_camera_state(image);
+      }
+    });
   }
 #endif
 }
diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp
index 3f84ef306e..60814e359d 100644
--- a/esphome/components/api/list_entities.cpp
+++ b/esphome/components/api/list_entities.cpp
@@ -40,8 +40,8 @@ LIST_ENTITIES_HANDLER(lock, lock::Lock, ListEntitiesLockResponse)
 #ifdef USE_VALVE
 LIST_ENTITIES_HANDLER(valve, valve::Valve, ListEntitiesValveResponse)
 #endif
-#ifdef USE_ESP32_CAMERA
-LIST_ENTITIES_HANDLER(camera, esp32_camera::ESP32Camera, ListEntitiesCameraResponse)
+#ifdef USE_CAMERA
+LIST_ENTITIES_HANDLER(camera, camera::Camera, ListEntitiesCameraResponse)
 #endif
 #ifdef USE_CLIMATE
 LIST_ENTITIES_HANDLER(climate, climate::Climate, ListEntitiesClimateResponse)
diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h
index b9506073d2..4c83ca0935 100644
--- a/esphome/components/api/list_entities.h
+++ b/esphome/components/api/list_entities.h
@@ -45,8 +45,8 @@ class ListEntitiesIterator : public ComponentIterator {
   bool on_text_sensor(text_sensor::TextSensor *entity) override;
 #endif
   bool on_service(UserServiceDescriptor *service) override;
-#ifdef USE_ESP32_CAMERA
-  bool on_camera(esp32_camera::ESP32Camera *entity) override;
+#ifdef USE_CAMERA
+  bool on_camera(camera::Camera *entity) override;
 #endif
 #ifdef USE_CLIMATE
   bool on_climate(climate::Climate *entity) override;
diff --git a/esphome/components/camera/__init__.py b/esphome/components/camera/__init__.py
new file mode 100644
index 0000000000..a19f7707af
--- /dev/null
+++ b/esphome/components/camera/__init__.py
@@ -0,0 +1 @@
+CODEOWNERS = ["@DT-art1", "@bdraco"]
diff --git a/esphome/components/camera/camera.cpp b/esphome/components/camera/camera.cpp
new file mode 100644
index 0000000000..3bd632af5c
--- /dev/null
+++ b/esphome/components/camera/camera.cpp
@@ -0,0 +1,22 @@
+#include "camera.h"
+
+namespace esphome {
+namespace camera {
+
+// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
+Camera *Camera::global_camera = nullptr;
+
+Camera::Camera() {
+  if (global_camera != nullptr) {
+    this->status_set_error("Multiple cameras are configured, but only one is supported.");
+    this->mark_failed();
+    return;
+  }
+
+  global_camera = this;
+}
+
+Camera *Camera::instance() { return global_camera; }
+
+}  // namespace camera
+}  // namespace esphome
diff --git a/esphome/components/camera/camera.h b/esphome/components/camera/camera.h
new file mode 100644
index 0000000000..fb9da58cc1
--- /dev/null
+++ b/esphome/components/camera/camera.h
@@ -0,0 +1,80 @@
+#pragma once
+
+#include "esphome/core/automation.h"
+#include "esphome/core/component.h"
+#include "esphome/core/entity_base.h"
+#include "esphome/core/helpers.h"
+
+namespace esphome {
+namespace camera {
+
+/** Different sources for filtering.
+ *  IDLE: Camera requests to send an image to the API.
+ *  API_REQUESTER: API requests a new image.
+ *  WEB_REQUESTER: ESP32 web server request an image. Ignored by API.
+ */
+enum CameraRequester : uint8_t { IDLE, API_REQUESTER, WEB_REQUESTER };
+
+/** Abstract camera image base class.
+ *  Encapsulates the JPEG encoded data and it is shared among
+ *  all connected clients.
+ */
+class CameraImage {
+ public:
+  virtual uint8_t *get_data_buffer() = 0;
+  virtual size_t get_data_length() = 0;
+  virtual bool was_requested_by(CameraRequester requester) const = 0;
+  virtual ~CameraImage() {}
+};
+
+/** Abstract image reader base class.
+ *  Keeps track of the data offset of the camera image and
+ *  how many bytes are remaining to read. When the image
+ *  is returned, the shared_ptr is reset and the camera can
+ *  reuse the memory of the camera image.
+ */
+class CameraImageReader {
+ public:
+  virtual void set_image(std::shared_ptr image) = 0;
+  virtual size_t available() const = 0;
+  virtual uint8_t *peek_data_buffer() = 0;
+  virtual void consume_data(size_t consumed) = 0;
+  virtual void return_image() = 0;
+  virtual ~CameraImageReader() {}
+};
+
+/** Abstract camera base class. Collaborates with API.
+ *  1) API server starts and installs callback (add_image_callback)
+ *     which is called by the camera when a new image is available.
+ *  2) New API client connects and creates a new image reader (create_image_reader).
+ *  3) API connection receives protobuf CameraImageRequest and calls request_image.
+ *  3.a) API connection receives protobuf CameraImageRequest and calls start_stream.
+ *  4) Camera implementation provides JPEG data in the CameraImage and calls callback.
+ *  5) API connection sets the image in the image reader.
+ *  6) API connection consumes data from the image reader and returns the image when finished.
+ *  7.a) Camera captures a new image and continues with 4) until start_stream is called.
+ */
+class Camera : public EntityBase, public Component {
+ public:
+  Camera();
+  // Camera implementation invokes callback to publish a new image.
+  virtual void add_image_callback(std::function)> &&callback) = 0;
+  /// Returns a new camera image reader that keeps track of the JPEG data in the camera image.
+  virtual CameraImageReader *create_image_reader() = 0;
+  // Connection, camera or web server requests one new JPEG image.
+  virtual void request_image(CameraRequester requester) = 0;
+  // Connection, camera or web server requests a stream of images.
+  virtual void start_stream(CameraRequester requester) = 0;
+  // Connection or web server stops the previously started stream.
+  virtual void stop_stream(CameraRequester requester) = 0;
+  virtual ~Camera() {}
+  /// The singleton instance of the camera implementation.
+  static Camera *instance();
+
+ protected:
+  // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
+  static Camera *global_camera;
+};
+
+}  // namespace camera
+}  // namespace esphome
diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py
index 8dc2ede372..138f318a5d 100644
--- a/esphome/components/esp32_camera/__init__.py
+++ b/esphome/components/esp32_camera/__init__.py
@@ -23,7 +23,7 @@ from esphome.core.entity_helpers import setup_entity
 
 DEPENDENCIES = ["esp32"]
 
-AUTO_LOAD = ["psram"]
+AUTO_LOAD = ["camera", "psram"]
 
 esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
 ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase)
@@ -283,6 +283,7 @@ SETTERS = {
 
 
 async def to_code(config):
+    cg.add_define("USE_CAMERA")
     var = cg.new_Pvariable(config[CONF_ID])
     await setup_entity(var, config, "camera")
     await cg.register_component(var, config)
diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp
index 243d3d3e47..eadb8a4408 100644
--- a/esphome/components/esp32_camera/esp32_camera.cpp
+++ b/esphome/components/esp32_camera/esp32_camera.cpp
@@ -14,8 +14,6 @@ static const char *const TAG = "esp32_camera";
 
 /* ---------------- public API (derivated) ---------------- */
 void ESP32Camera::setup() {
-  global_esp32_camera = this;
-
 #ifdef USE_I2C
   if (this->i2c_bus_ != nullptr) {
     this->config_.sccb_i2c_port = this->i2c_bus_->get_port();
@@ -43,7 +41,7 @@ void ESP32Camera::setup() {
   xTaskCreatePinnedToCore(&ESP32Camera::framebuffer_task,
                           "framebuffer_task",  // name
                           1024,                // stack size
-                          nullptr,             // task pv params
+                          this,                // task pv params
                           1,                   // priority
                           nullptr,             // handle
                           1                    // core
@@ -176,7 +174,7 @@ void ESP32Camera::loop() {
   const uint32_t now = App.get_loop_component_start_time();
   if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) {
     this->last_idle_request_ = now;
-    this->request_image(IDLE);
+    this->request_image(camera::IDLE);
   }
 
   // Check if we should fetch a new image
@@ -202,7 +200,7 @@ void ESP32Camera::loop() {
     xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY);
     return;
   }
-  this->current_image_ = std::make_shared(fb, this->single_requesters_ | this->stream_requesters_);
+  this->current_image_ = std::make_shared(fb, this->single_requesters_ | this->stream_requesters_);
 
   ESP_LOGD(TAG, "Got Image: len=%u", fb->len);
   this->new_image_callback_.call(this->current_image_);
@@ -225,8 +223,6 @@ ESP32Camera::ESP32Camera() {
   this->config_.fb_count = 1;
   this->config_.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
   this->config_.fb_location = CAMERA_FB_IN_PSRAM;
-
-  global_esp32_camera = this;
 }
 
 /* ---------------- setters ---------------- */
@@ -356,7 +352,7 @@ void ESP32Camera::set_frame_buffer_count(uint8_t fb_count) {
 }
 
 /* ---------------- public API (specific) ---------------- */
-void ESP32Camera::add_image_callback(std::function)> &&callback) {
+void ESP32Camera::add_image_callback(std::function)> &&callback) {
   this->new_image_callback_.add(std::move(callback));
 }
 void ESP32Camera::add_stream_start_callback(std::function &&callback) {
@@ -365,15 +361,16 @@ void ESP32Camera::add_stream_start_callback(std::function &&callback) {
 void ESP32Camera::add_stream_stop_callback(std::function &&callback) {
   this->stream_stop_callback_.add(std::move(callback));
 }
-void ESP32Camera::start_stream(CameraRequester requester) {
+void ESP32Camera::start_stream(camera::CameraRequester requester) {
   this->stream_start_callback_.call();
   this->stream_requesters_ |= (1U << requester);
 }
-void ESP32Camera::stop_stream(CameraRequester requester) {
+void ESP32Camera::stop_stream(camera::CameraRequester requester) {
   this->stream_stop_callback_.call();
   this->stream_requesters_ &= ~(1U << requester);
 }
-void ESP32Camera::request_image(CameraRequester requester) { this->single_requesters_ |= (1U << requester); }
+void ESP32Camera::request_image(camera::CameraRequester requester) { this->single_requesters_ |= (1U << requester); }
+camera::CameraImageReader *ESP32Camera::create_image_reader() { return new ESP32CameraImageReader; }
 void ESP32Camera::update_camera_parameters() {
   sensor_t *s = esp_camera_sensor_get();
   /* update image */
@@ -402,39 +399,39 @@ void ESP32Camera::update_camera_parameters() {
 bool ESP32Camera::has_requested_image_() const { return this->single_requesters_ || this->stream_requesters_; }
 bool ESP32Camera::can_return_image_() const { return this->current_image_.use_count() == 1; }
 void ESP32Camera::framebuffer_task(void *pv) {
+  ESP32Camera *that = (ESP32Camera *) pv;
   while (true) {
     camera_fb_t *framebuffer = esp_camera_fb_get();
-    xQueueSend(global_esp32_camera->framebuffer_get_queue_, &framebuffer, portMAX_DELAY);
+    xQueueSend(that->framebuffer_get_queue_, &framebuffer, portMAX_DELAY);
     // return is no-op for config with 1 fb
-    xQueueReceive(global_esp32_camera->framebuffer_return_queue_, &framebuffer, portMAX_DELAY);
+    xQueueReceive(that->framebuffer_return_queue_, &framebuffer, portMAX_DELAY);
     esp_camera_fb_return(framebuffer);
   }
 }
 
-ESP32Camera *global_esp32_camera;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
-
-/* ---------------- CameraImageReader class ---------------- */
-void CameraImageReader::set_image(std::shared_ptr image) {
-  this->image_ = std::move(image);
+/* ---------------- ESP32CameraImageReader class ----------- */
+void ESP32CameraImageReader::set_image(std::shared_ptr image) {
+  this->image_ = std::static_pointer_cast(image);
   this->offset_ = 0;
 }
-size_t CameraImageReader::available() const {
+size_t ESP32CameraImageReader::available() const {
   if (!this->image_)
     return 0;
 
   return this->image_->get_data_length() - this->offset_;
 }
-void CameraImageReader::return_image() { this->image_.reset(); }
-void CameraImageReader::consume_data(size_t consumed) { this->offset_ += consumed; }
-uint8_t *CameraImageReader::peek_data_buffer() { return this->image_->get_data_buffer() + this->offset_; }
+void ESP32CameraImageReader::return_image() { this->image_.reset(); }
+void ESP32CameraImageReader::consume_data(size_t consumed) { this->offset_ += consumed; }
+uint8_t *ESP32CameraImageReader::peek_data_buffer() { return this->image_->get_data_buffer() + this->offset_; }
 
-/* ---------------- CameraImage class ---------------- */
-CameraImage::CameraImage(camera_fb_t *buffer, uint8_t requesters) : buffer_(buffer), requesters_(requesters) {}
+/* ---------------- ESP32CameraImage class ----------- */
+ESP32CameraImage::ESP32CameraImage(camera_fb_t *buffer, uint8_t requesters)
+    : buffer_(buffer), requesters_(requesters) {}
 
-camera_fb_t *CameraImage::get_raw_buffer() { return this->buffer_; }
-uint8_t *CameraImage::get_data_buffer() { return this->buffer_->buf; }
-size_t CameraImage::get_data_length() { return this->buffer_->len; }
-bool CameraImage::was_requested_by(CameraRequester requester) const {
+camera_fb_t *ESP32CameraImage::get_raw_buffer() { return this->buffer_; }
+uint8_t *ESP32CameraImage::get_data_buffer() { return this->buffer_->buf; }
+size_t ESP32CameraImage::get_data_length() { return this->buffer_->len; }
+bool ESP32CameraImage::was_requested_by(camera::CameraRequester requester) const {
   return (this->requesters_ & (1 << requester)) != 0;
 }
 
diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h
index 75139ba400..8ce3faf039 100644
--- a/esphome/components/esp32_camera/esp32_camera.h
+++ b/esphome/components/esp32_camera/esp32_camera.h
@@ -7,7 +7,7 @@
 #include 
 #include "esphome/core/automation.h"
 #include "esphome/core/component.h"
-#include "esphome/core/entity_base.h"
+#include "esphome/components/camera/camera.h"
 #include "esphome/core/helpers.h"
 
 #ifdef USE_I2C
@@ -19,9 +19,6 @@ namespace esp32_camera {
 
 class ESP32Camera;
 
-/* ---------------- enum classes ---------------- */
-enum CameraRequester { IDLE, API_REQUESTER, WEB_REQUESTER };
-
 enum ESP32CameraFrameSize {
   ESP32_CAMERA_SIZE_160X120,    // QQVGA
   ESP32_CAMERA_SIZE_176X144,    // QCIF
@@ -77,13 +74,13 @@ enum ESP32SpecialEffect {
 };
 
 /* ---------------- CameraImage class ---------------- */
-class CameraImage {
+class ESP32CameraImage : public camera::CameraImage {
  public:
-  CameraImage(camera_fb_t *buffer, uint8_t requester);
+  ESP32CameraImage(camera_fb_t *buffer, uint8_t requester);
   camera_fb_t *get_raw_buffer();
-  uint8_t *get_data_buffer();
-  size_t get_data_length();
-  bool was_requested_by(CameraRequester requester) const;
+  uint8_t *get_data_buffer() override;
+  size_t get_data_length() override;
+  bool was_requested_by(camera::CameraRequester requester) const override;
 
  protected:
   camera_fb_t *buffer_;
@@ -96,21 +93,21 @@ struct CameraImageData {
 };
 
 /* ---------------- CameraImageReader class ---------------- */
-class CameraImageReader {
+class ESP32CameraImageReader : public camera::CameraImageReader {
  public:
-  void set_image(std::shared_ptr image);
-  size_t available() const;
-  uint8_t *peek_data_buffer();
-  void consume_data(size_t consumed);
-  void return_image();
+  void set_image(std::shared_ptr image) override;
+  size_t available() const override;
+  uint8_t *peek_data_buffer() override;
+  void consume_data(size_t consumed) override;
+  void return_image() override;
 
  protected:
-  std::shared_ptr image_;
+  std::shared_ptr image_;
   size_t offset_{0};
 };
 
 /* ---------------- ESP32Camera class ---------------- */
-class ESP32Camera : public EntityBase, public Component {
+class ESP32Camera : public camera::Camera {
  public:
   ESP32Camera();
 
@@ -162,14 +159,15 @@ class ESP32Camera : public EntityBase, public Component {
   void dump_config() override;
   float get_setup_priority() const override;
   /* public API (specific) */
-  void start_stream(CameraRequester requester);
-  void stop_stream(CameraRequester requester);
-  void request_image(CameraRequester requester);
+  void start_stream(camera::CameraRequester requester) override;
+  void stop_stream(camera::CameraRequester requester) override;
+  void request_image(camera::CameraRequester requester) override;
   void update_camera_parameters();
 
-  void add_image_callback(std::function)> &&callback);
+  void add_image_callback(std::function)> &&callback) override;
   void add_stream_start_callback(std::function &&callback);
   void add_stream_stop_callback(std::function &&callback);
+  camera::CameraImageReader *create_image_reader() override;
 
  protected:
   /* internal methods */
@@ -206,12 +204,12 @@ class ESP32Camera : public EntityBase, public Component {
   uint32_t idle_update_interval_{15000};
 
   esp_err_t init_error_{ESP_OK};
-  std::shared_ptr current_image_;
+  std::shared_ptr current_image_;
   uint8_t single_requesters_{0};
   uint8_t stream_requesters_{0};
   QueueHandle_t framebuffer_get_queue_;
   QueueHandle_t framebuffer_return_queue_;
-  CallbackManager)> new_image_callback_{};
+  CallbackManager)> new_image_callback_{};
   CallbackManager stream_start_callback_{};
   CallbackManager stream_stop_callback_{};
 
@@ -222,13 +220,10 @@ class ESP32Camera : public EntityBase, public Component {
 #endif  // USE_I2C
 };
 
-// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
-extern ESP32Camera *global_esp32_camera;
-
 class ESP32CameraImageTrigger : public Trigger {
  public:
   explicit ESP32CameraImageTrigger(ESP32Camera *parent) {
-    parent->add_image_callback([this](const std::shared_ptr &image) {
+    parent->add_image_callback([this](const std::shared_ptr &image) {
       CameraImageData camera_image_data{};
       camera_image_data.length = image->get_data_length();
       camera_image_data.data = image->get_data_buffer();
diff --git a/esphome/components/esp32_camera_web_server/__init__.py b/esphome/components/esp32_camera_web_server/__init__.py
index df137c8ff2..a6a7ac3630 100644
--- a/esphome/components/esp32_camera_web_server/__init__.py
+++ b/esphome/components/esp32_camera_web_server/__init__.py
@@ -3,7 +3,8 @@ import esphome.config_validation as cv
 from esphome.const import CONF_ID, CONF_MODE, CONF_PORT
 
 CODEOWNERS = ["@ayufan"]
-DEPENDENCIES = ["esp32_camera", "network"]
+AUTO_LOAD = ["camera"]
+DEPENDENCIES = ["network"]
 MULTI_CONF = True
 
 esp32_camera_web_server_ns = cg.esphome_ns.namespace("esp32_camera_web_server")
diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.cpp b/esphome/components/esp32_camera_web_server/camera_web_server.cpp
index 0a83128908..1b81989296 100644
--- a/esphome/components/esp32_camera_web_server/camera_web_server.cpp
+++ b/esphome/components/esp32_camera_web_server/camera_web_server.cpp
@@ -40,7 +40,7 @@ CameraWebServer::CameraWebServer() {}
 CameraWebServer::~CameraWebServer() {}
 
 void CameraWebServer::setup() {
-  if (!esp32_camera::global_esp32_camera || esp32_camera::global_esp32_camera->is_failed()) {
+  if (!camera::Camera::instance() || camera::Camera::instance()->is_failed()) {
     this->mark_failed();
     return;
   }
@@ -67,8 +67,8 @@ void CameraWebServer::setup() {
 
   httpd_register_uri_handler(this->httpd_, &uri);
 
-  esp32_camera::global_esp32_camera->add_image_callback([this](std::shared_ptr image) {
-    if (this->running_ && image->was_requested_by(esp32_camera::WEB_REQUESTER)) {
+  camera::Camera::instance()->add_image_callback([this](std::shared_ptr image) {
+    if (this->running_ && image->was_requested_by(camera::WEB_REQUESTER)) {
       this->image_ = std::move(image);
       xSemaphoreGive(this->semaphore_);
     }
@@ -108,8 +108,8 @@ void CameraWebServer::loop() {
   }
 }
 
-std::shared_ptr CameraWebServer::wait_for_image_() {
-  std::shared_ptr image;
+std::shared_ptr CameraWebServer::wait_for_image_() {
+  std::shared_ptr image;
   image.swap(this->image_);
 
   if (!image) {
@@ -172,7 +172,7 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) {
   uint32_t last_frame = millis();
   uint32_t frames = 0;
 
-  esp32_camera::global_esp32_camera->start_stream(esphome::esp32_camera::WEB_REQUESTER);
+  camera::Camera::instance()->start_stream(esphome::camera::WEB_REQUESTER);
 
   while (res == ESP_OK && this->running_) {
     auto image = this->wait_for_image_();
@@ -205,7 +205,7 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) {
     res = httpd_send_all(req, STREAM_ERROR, strlen(STREAM_ERROR));
   }
 
-  esp32_camera::global_esp32_camera->stop_stream(esphome::esp32_camera::WEB_REQUESTER);
+  camera::Camera::instance()->stop_stream(esphome::camera::WEB_REQUESTER);
 
   ESP_LOGI(TAG, "STREAM: closed. Frames: %" PRIu32, frames);
 
@@ -215,7 +215,7 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) {
 esp_err_t CameraWebServer::snapshot_handler_(struct httpd_req *req) {
   esp_err_t res = ESP_OK;
 
-  esp32_camera::global_esp32_camera->request_image(esphome::esp32_camera::WEB_REQUESTER);
+  camera::Camera::instance()->request_image(esphome::camera::WEB_REQUESTER);
 
   auto image = this->wait_for_image_();
 
diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.h b/esphome/components/esp32_camera_web_server/camera_web_server.h
index 3ba8f31dd7..e70246745c 100644
--- a/esphome/components/esp32_camera_web_server/camera_web_server.h
+++ b/esphome/components/esp32_camera_web_server/camera_web_server.h
@@ -6,7 +6,7 @@
 #include 
 #include 
 
-#include "esphome/components/esp32_camera/esp32_camera.h"
+#include "esphome/components/camera/camera.h"
 #include "esphome/core/component.h"
 #include "esphome/core/helpers.h"
 #include "esphome/core/preferences.h"
@@ -32,7 +32,7 @@ class CameraWebServer : public Component {
   void loop() override;
 
  protected:
-  std::shared_ptr wait_for_image_();
+  std::shared_ptr wait_for_image_();
   esp_err_t handler_(struct httpd_req *req);
   esp_err_t streaming_handler_(struct httpd_req *req);
   esp_err_t snapshot_handler_(struct httpd_req *req);
@@ -40,7 +40,7 @@ class CameraWebServer : public Component {
   uint16_t port_{0};
   void *httpd_{nullptr};
   SemaphoreHandle_t semaphore_;
-  std::shared_ptr image_;
+  std::shared_ptr image_;
   bool running_{false};
   Mode mode_{STREAM};
 };
diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp
index b06c964b7c..aab5c2a72d 100644
--- a/esphome/core/component_iterator.cpp
+++ b/esphome/core/component_iterator.cpp
@@ -158,16 +158,16 @@ void ComponentIterator::advance() {
       }
       break;
 #endif
-#ifdef USE_ESP32_CAMERA
+#ifdef USE_CAMERA
     case IteratorState::CAMERA:
-      if (esp32_camera::global_esp32_camera == nullptr) {
+      if (camera::Camera::instance() == nullptr) {
         advance_platform = true;
       } else {
-        if (esp32_camera::global_esp32_camera->is_internal() && !this->include_internal_) {
+        if (camera::Camera::instance()->is_internal() && !this->include_internal_) {
           advance_platform = success = true;
           break;
         } else {
-          advance_platform = success = this->on_camera(esp32_camera::global_esp32_camera);
+          advance_platform = success = this->on_camera(camera::Camera::instance());
         }
       }
       break;
@@ -386,8 +386,8 @@ bool ComponentIterator::on_begin() { return true; }
 #ifdef USE_API
 bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; }
 #endif
-#ifdef USE_ESP32_CAMERA
-bool ComponentIterator::on_camera(esp32_camera::ESP32Camera *camera) { return true; }
+#ifdef USE_CAMERA
+bool ComponentIterator::on_camera(camera::Camera *camera) { return true; }
 #endif
 #ifdef USE_MEDIA_PLAYER
 bool ComponentIterator::on_media_player(media_player::MediaPlayer *media_player) { return true; }
diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h
index 4b41872db7..eda786be7f 100644
--- a/esphome/core/component_iterator.h
+++ b/esphome/core/component_iterator.h
@@ -4,8 +4,8 @@
 #include "esphome/core/controller.h"
 #include "esphome/core/helpers.h"
 
-#ifdef USE_ESP32_CAMERA
-#include "esphome/components/esp32_camera/esp32_camera.h"
+#ifdef USE_CAMERA
+#include "esphome/components/camera/camera.h"
 #endif
 
 namespace esphome {
@@ -48,8 +48,8 @@ class ComponentIterator {
 #ifdef USE_API
   virtual bool on_service(api::UserServiceDescriptor *service);
 #endif
-#ifdef USE_ESP32_CAMERA
-  virtual bool on_camera(esp32_camera::ESP32Camera *camera);
+#ifdef USE_CAMERA
+  virtual bool on_camera(camera::Camera *camera);
 #endif
 #ifdef USE_CLIMATE
   virtual bool on_climate(climate::Climate *climate) = 0;
@@ -125,7 +125,7 @@ class ComponentIterator {
 #ifdef USE_API
     SERVICE,
 #endif
-#ifdef USE_ESP32_CAMERA
+#ifdef USE_CAMERA
     CAMERA,
 #endif
 #ifdef USE_CLIMATE
diff --git a/esphome/core/defines.h b/esphome/core/defines.h
index 320b40dc90..4115b97391 100644
--- a/esphome/core/defines.h
+++ b/esphome/core/defines.h
@@ -23,6 +23,7 @@
 #define USE_AREAS
 #define USE_BINARY_SENSOR
 #define USE_BUTTON
+#define USE_CAMERA
 #define USE_CLIMATE
 #define USE_COVER
 #define USE_DATETIME
@@ -144,7 +145,6 @@
 #define USE_ESP32_BLE
 #define USE_ESP32_BLE_CLIENT
 #define USE_ESP32_BLE_SERVER
-#define USE_ESP32_CAMERA
 #define USE_I2C
 #define USE_IMPROV
 #define USE_MICROPHONE
diff --git a/tests/components/camera/common.yaml b/tests/components/camera/common.yaml
new file mode 100644
index 0000000000..3daf1e8565
--- /dev/null
+++ b/tests/components/camera/common.yaml
@@ -0,0 +1,18 @@
+esphome:
+  includes:
+    - ../../../esphome/components/camera/
+
+script:
+  - id: interface_compile_check
+    then:
+      - lambda: |-
+            using namespace esphome::camera;
+            class MockCamera : public Camera {
+              public:
+                void add_image_callback(std::function)> &&callback) override {}
+                CameraImageReader *create_image_reader() override { return 0; }
+                void request_image(CameraRequester requester) override {}
+                void start_stream(CameraRequester requester) override {}
+                void stop_stream(CameraRequester requester) override {}
+            };
+            MockCamera* camera = new MockCamera();
diff --git a/tests/components/camera/test.esp32-ard.yaml b/tests/components/camera/test.esp32-ard.yaml
new file mode 100644
index 0000000000..dade44d145
--- /dev/null
+++ b/tests/components/camera/test.esp32-ard.yaml
@@ -0,0 +1 @@
+<<: !include common.yaml
diff --git a/tests/components/camera/test.esp32-idf.yaml b/tests/components/camera/test.esp32-idf.yaml
new file mode 100644
index 0000000000..dade44d145
--- /dev/null
+++ b/tests/components/camera/test.esp32-idf.yaml
@@ -0,0 +1 @@
+<<: !include common.yaml

From 364b6ca8d0f68449b8dc375063b4bdce5106c462 Mon Sep 17 00:00:00 2001
From: Keith Burzinski 
Date: Sun, 6 Jul 2025 22:54:19 -0500
Subject: [PATCH 250/293] [scd4x] Memory optimization (#9358)

---
 esphome/components/scd4x/scd4x.cpp |  9 ++++-----
 esphome/components/scd4x/scd4x.h   | 18 ++++++++----------
 2 files changed, 12 insertions(+), 15 deletions(-)

diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp
index 4a700b70c2..06db70e3f3 100644
--- a/esphome/components/scd4x/scd4x.cpp
+++ b/esphome/components/scd4x/scd4x.cpp
@@ -58,7 +58,7 @@ void SCD4XComponent::setup() {
       }
 
       // If pressure compensation available use it, else use altitude
-      if (this->ambient_pressure_compensation_) {
+      if (this->ambient_pressure_) {
         if (!this->update_ambient_pressure_compensation_(this->ambient_pressure_)) {
           ESP_LOGE(TAG, "Error setting ambient pressure compensation");
           this->error_code_ = MEASUREMENT_INIT_FAILED;
@@ -137,7 +137,7 @@ void SCD4XComponent::dump_config() {
     ESP_LOGCONFIG(TAG, "  Dynamic ambient pressure compensation using '%s'",
                   this->ambient_pressure_source_->get_name().c_str());
   } else {
-    if (this->ambient_pressure_compensation_) {
+    if (this->ambient_pressure_) {
       ESP_LOGCONFIG(TAG,
                     "  Altitude compensation disabled\n"
                     "  Ambient pressure compensation: %dmBar",
@@ -230,7 +230,7 @@ bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentrati
       // frc takes 400 ms
       // because this method will be used very rarly
       // the simple approach with delay is ok
-      delay(400);  // NOLINT'
+      delay(400);  // NOLINT
       if (!this->start_measurement_()) {
         return false;
       } else {
@@ -267,8 +267,7 @@ bool SCD4XComponent::factory_reset() {
 }
 
 void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_hpa) {
-  ambient_pressure_compensation_ = true;
-  uint16_t new_ambient_pressure = (uint16_t) pressure_in_hpa;
+  uint16_t new_ambient_pressure = static_cast(pressure_in_hpa);
   if (!this->initialized_) {
     this->ambient_pressure_ = new_ambient_pressure;
     return;
diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h
index 237d226107..ab5d72aeec 100644
--- a/esphome/components/scd4x/scd4x.h
+++ b/esphome/components/scd4x/scd4x.h
@@ -46,19 +46,17 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri
   bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa);
   bool start_measurement_();
 
-  uint16_t altitude_compensation_;
-  uint16_t ambient_pressure_;
-  bool initialized_{false};
-  bool ambient_pressure_compensation_;
-  bool enable_asc_;
-  float temperature_offset_;
-  ErrorCode error_code_;
-  MeasurementMode measurement_mode_{PERIODIC};
   sensor::Sensor *co2_sensor_{nullptr};
   sensor::Sensor *temperature_sensor_{nullptr};
   sensor::Sensor *humidity_sensor_{nullptr};
-  // used for compensation
-  sensor::Sensor *ambient_pressure_source_{nullptr};
+  sensor::Sensor *ambient_pressure_source_{nullptr};  // used for compensation
+  float temperature_offset_;
+  uint16_t altitude_compensation_{0};
+  uint16_t ambient_pressure_{0};  // Per datasheet, valid values are 700 to 1200 hPa; 0 is a valid sentinel value
+  bool initialized_{false};
+  bool enable_asc_{false};
+  ErrorCode error_code_;
+  MeasurementMode measurement_mode_{PERIODIC};
 };
 
 }  // namespace scd4x

From 2510b5ffb57fd527efee441039db3346503d8542 Mon Sep 17 00:00:00 2001
From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com>
Date: Mon, 7 Jul 2025 06:07:03 +0200
Subject: [PATCH 251/293] [nextion] Replace boolean flags with bitfields to
 optimize memory usage (#9359)

---
 esphome/components/nextion/nextion.cpp        | 62 +++++++++----------
 esphome/components/nextion/nextion.h          | 23 ++++---
 .../components/nextion/nextion_commands.cpp   |  8 +--
 esphome/components/nextion/nextion_upload.cpp |  4 +-
 .../nextion/nextion_upload_arduino.cpp        |  8 +--
 .../components/nextion/nextion_upload_idf.cpp |  6 +-
 6 files changed, 58 insertions(+), 53 deletions(-)

diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp
index bcb1aced9a..d95238bbb4 100644
--- a/esphome/components/nextion/nextion.cpp
+++ b/esphome/components/nextion/nextion.cpp
@@ -11,7 +11,7 @@ static const char *const TAG = "nextion";
 
 void Nextion::setup() {
   this->is_setup_ = false;
-  this->ignore_is_setup_ = true;
+  this->connection_state_.ignore_is_setup_ = true;
 
   // Wake up the nextion
   this->send_command_("bkcmd=0");
@@ -23,16 +23,16 @@ void Nextion::setup() {
   // Reboot it
   this->send_command_("rest");
 
-  this->ignore_is_setup_ = false;
+  this->connection_state_.ignore_is_setup_ = false;
 }
 
 bool Nextion::send_command_(const std::string &command) {
-  if (!this->ignore_is_setup_ && !this->is_setup()) {
+  if (!this->connection_state_.ignore_is_setup_ && !this->is_setup()) {
     return false;
   }
 
 #ifdef USE_NEXTION_COMMAND_SPACING
-  if (!this->ignore_is_setup_ && !this->command_pacer_.can_send()) {
+  if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send()) {
     ESP_LOGN(TAG, "Command spacing: delaying command '%s'", command.c_str());
     return false;
   }
@@ -48,7 +48,7 @@ bool Nextion::send_command_(const std::string &command) {
 }
 
 bool Nextion::check_connect_() {
-  if (this->is_connected_)
+  if (this->connection_state_.is_connected_)
     return true;
 
   // Check if the handshake should be skipped for the Nextion connection
@@ -56,7 +56,7 @@ bool Nextion::check_connect_() {
     // Log the connection status without handshake
     ESP_LOGW(TAG, "Connected (no handshake)");
     // Set the connection status to true
-    this->is_connected_ = true;
+    this->connection_state_.is_connected_ = true;
     // Return true indicating the connection is set
     return true;
   }
@@ -64,7 +64,7 @@ bool Nextion::check_connect_() {
   if (this->comok_sent_ == 0) {
     this->reset_(false);
 
-    this->ignore_is_setup_ = true;
+    this->connection_state_.ignore_is_setup_ = true;
     this->send_command_("boguscommand=0");  // bogus command. needed sometimes after updating
     if (this->exit_reparse_on_start_) {
       this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN");
@@ -72,7 +72,7 @@ bool Nextion::check_connect_() {
     this->send_command_("connect");
 
     this->comok_sent_ = App.get_loop_component_start_time();
-    this->ignore_is_setup_ = false;
+    this->connection_state_.ignore_is_setup_ = false;
 
     return false;
   }
@@ -101,9 +101,9 @@ bool Nextion::check_connect_() {
     return false;
   }
 
-  this->ignore_is_setup_ = true;
+  this->connection_state_.ignore_is_setup_ = true;
   ESP_LOGI(TAG, "Connected");
-  this->is_connected_ = true;
+  this->connection_state_.is_connected_ = true;
 
   ESP_LOGN(TAG, "connect: %s", response.c_str());
 
@@ -127,7 +127,7 @@ bool Nextion::check_connect_() {
     ESP_LOGE(TAG, "Bad connect value: '%s'", response.c_str());
   }
 
-  this->ignore_is_setup_ = false;
+  this->connection_state_.ignore_is_setup_ = false;
   this->dump_config();
   return true;
 }
@@ -158,7 +158,7 @@ void Nextion::dump_config() {
   ESP_LOGCONFIG(TAG,
                 "  Wake On Touch:  %s\n"
                 "  Exit reparse:   %s",
-                YESNO(this->auto_wake_on_touch_), YESNO(this->exit_reparse_on_start_));
+                YESNO(this->connection_state_.auto_wake_on_touch_), YESNO(this->exit_reparse_on_start_));
 #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
   ESP_LOGCONFIG(TAG, "  Max commands per loop: %u", this->max_commands_per_loop_);
 #endif  // USE_NEXTION_MAX_COMMANDS_PER_LOOP
@@ -221,7 +221,7 @@ void Nextion::add_buffer_overflow_event_callback(std::function &&callbac
 }
 
 void Nextion::update_all_components() {
-  if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping())
+  if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping())
     return;
 
   for (auto *binarysensortype : this->binarysensortype_) {
@@ -239,7 +239,7 @@ void Nextion::update_all_components() {
 }
 
 bool Nextion::send_command(const char *command) {
-  if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping())
+  if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping())
     return false;
 
   if (this->send_command_(command)) {
@@ -250,7 +250,7 @@ bool Nextion::send_command(const char *command) {
 }
 
 bool Nextion::send_command_printf(const char *format, ...) {
-  if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping())
+  if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping())
     return false;
 
   char buffer[256];
@@ -291,12 +291,12 @@ void Nextion::print_queue_members_() {
 #endif
 
 void Nextion::loop() {
-  if (!this->check_connect_() || this->is_updating_)
+  if (!this->check_connect_() || this->connection_state_.is_updating_)
     return;
 
-  if (this->nextion_reports_is_setup_ && !this->sent_setup_commands_) {
-    this->ignore_is_setup_ = true;
-    this->sent_setup_commands_ = true;
+  if (this->connection_state_.nextion_reports_is_setup_ && !this->connection_state_.sent_setup_commands_) {
+    this->connection_state_.ignore_is_setup_ = true;
+    this->connection_state_.sent_setup_commands_ = true;
     this->send_command_("bkcmd=3");  // Always, returns 0x00 to 0x23 result of serial command.
 
     if (this->brightness_.has_value()) {
@@ -314,19 +314,19 @@ void Nextion::loop() {
       this->set_wake_up_page(this->wake_up_page_);
     }
 
-    this->ignore_is_setup_ = false;
+    this->connection_state_.ignore_is_setup_ = false;
   }
 
   this->process_serial_();            // Receive serial data
   this->process_nextion_commands_();  // Process nextion return commands
 
-  if (!this->nextion_reports_is_setup_) {
+  if (!this->connection_state_.nextion_reports_is_setup_) {
     if (this->started_ms_ == 0)
       this->started_ms_ = App.get_loop_component_start_time();
 
     if (this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) {
       ESP_LOGD(TAG, "Manual ready set");
-      this->nextion_reports_is_setup_ = true;
+      this->connection_state_.nextion_reports_is_setup_ = true;
     }
   }
 
@@ -669,7 +669,7 @@ void Nextion::process_nextion_commands_() {
       case 0x88:  // system successful start up
       {
         ESP_LOGD(TAG, "System start: %zu", to_process_length);
-        this->nextion_reports_is_setup_ = true;
+        this->connection_state_.nextion_reports_is_setup_ = true;
         break;
       }
       case 0x89: {  // start SD card upgrade
@@ -1052,7 +1052,7 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) {
  * @param command
  */
 void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command) {
-  if ((!this->is_setup() && !this->ignore_is_setup_) || command.empty())
+  if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || command.empty())
     return;
 
   if (this->send_command_(command)) {
@@ -1095,7 +1095,7 @@ void Nextion::add_no_result_to_queue_with_pending_command_(const std::string &va
 
 bool Nextion::add_no_result_to_queue_with_ignore_sleep_printf_(const std::string &variable_name, const char *format,
                                                                ...) {
-  if ((!this->is_setup() && !this->ignore_is_setup_))
+  if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_))
     return false;
 
   char buffer[256];
@@ -1120,7 +1120,7 @@ bool Nextion::add_no_result_to_queue_with_ignore_sleep_printf_(const std::string
  * @param ... The format arguments
  */
 bool Nextion::add_no_result_to_queue_with_printf_(const std::string &variable_name, const char *format, ...) {
-  if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping())
+  if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping())
     return false;
 
   char buffer[256];
@@ -1159,7 +1159,7 @@ void Nextion::add_no_result_to_queue_with_set(const std::string &variable_name,
 void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &variable_name,
                                                         const std::string &variable_name_to_send, int32_t state_value,
                                                         bool is_sleep_safe) {
-  if ((!this->is_setup() && !this->ignore_is_setup_) || (!is_sleep_safe && this->is_sleeping()))
+  if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || (!is_sleep_safe && this->is_sleeping()))
     return;
 
   this->add_no_result_to_queue_with_ignore_sleep_printf_(variable_name, "%s=%" PRId32, variable_name_to_send.c_str(),
@@ -1187,7 +1187,7 @@ void Nextion::add_no_result_to_queue_with_set(const std::string &variable_name,
 void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &variable_name,
                                                         const std::string &variable_name_to_send,
                                                         const std::string &state_value, bool is_sleep_safe) {
-  if ((!this->is_setup() && !this->ignore_is_setup_) || (!is_sleep_safe && this->is_sleeping()))
+  if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || (!is_sleep_safe && this->is_sleeping()))
     return;
 
   this->add_no_result_to_queue_with_printf_(variable_name, "%s=\"%s\"", variable_name_to_send.c_str(),
@@ -1204,7 +1204,7 @@ void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &varia
  * @param component Pointer to the Nextion component that will handle the response.
  */
 void Nextion::add_to_get_queue(NextionComponentBase *component) {
-  if ((!this->is_setup() && !this->ignore_is_setup_))
+  if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_))
     return;
 
 #ifdef USE_NEXTION_MAX_QUEUE_SIZE
@@ -1244,7 +1244,7 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) {
  * @param buffer_size The buffer data
  */
 void Nextion::add_addt_command_to_queue(NextionComponentBase *component) {
-  if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping())
+  if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping())
     return;
 
   RAMAllocator allocator;
@@ -1285,7 +1285,7 @@ void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = write
 ESPDEPRECATED("set_wait_for_ack(bool) deprecated, no effect", "v1.20")
 void Nextion::set_wait_for_ack(bool wait_for_ack) { ESP_LOGE(TAG, "Deprecated"); }
 
-bool Nextion::is_updating() { return this->is_updating_; }
+bool Nextion::is_updating() { return this->connection_state_.is_updating_; }
 
 }  // namespace nextion
 }  // namespace esphome
diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h
index f5fa26b98c..0ce9429594 100644
--- a/esphome/components/nextion/nextion.h
+++ b/esphome/components/nextion/nextion.h
@@ -1302,7 +1302,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
    * @return true if the Nextion display is connected and ready to receive commands
    * @return false if the display is not yet connected or connection was lost
    */
-  bool is_connected() { return this->is_connected_; }
+  bool is_connected() { return this->connection_state_.is_connected_; }
 
  protected:
 #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
@@ -1336,21 +1336,28 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
   bool remove_from_q_(bool report_empty = true);
 
   /**
-   * @brief
-   * Sends commands ignoring of the Nextion has been setup.
+   * @brief Status flags for Nextion display state management
+   *
+   * Uses bitfields to pack multiple boolean states into a single byte,
+   * saving 5 bytes of RAM compared to individual bool variables.
    */
-  bool ignore_is_setup_ = false;
+  struct {
+    uint8_t is_connected_ : 1;              ///< Connection established with Nextion display
+    uint8_t sent_setup_commands_ : 1;       ///< Initial setup commands have been sent
+    uint8_t ignore_is_setup_ : 1;           ///< Temporarily ignore setup state for special operations
+    uint8_t nextion_reports_is_setup_ : 1;  ///< Nextion has reported successful initialization
+    uint8_t is_updating_ : 1;               ///< TFT firmware update is currently in progress
+    uint8_t auto_wake_on_touch_ : 1;        ///< Display should wake automatically on touch (default: true)
+    uint8_t reserved_ : 2;                  ///< Reserved bits for future flag additions
+  } connection_state_{};                    ///< Zero-initialized status flags (all start as false)
 
-  bool nextion_reports_is_setup_ = false;
   void process_nextion_commands_();
   void process_serial_();
-  bool is_updating_ = false;
   uint16_t touch_sleep_timeout_ = 0;
   uint8_t wake_up_page_ = 255;
 #ifdef USE_NEXTION_CONF_START_UP_PAGE
   uint8_t start_up_page_ = 255;
 #endif  // USE_NEXTION_CONF_START_UP_PAGE
-  bool auto_wake_on_touch_ = true;
   bool exit_reparse_on_start_ = false;
   bool skip_connection_handshake_ = false;
 
@@ -1472,11 +1479,9 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
   void reset_(bool reset_nextion = true);
 
   std::string command_data_;
-  bool is_connected_ = false;
   const uint16_t startup_override_ms_ = 8000;
   const uint16_t max_q_age_ms_ = 8000;
   uint32_t started_ms_ = 0;
-  bool sent_setup_commands_ = false;
 };
 
 }  // namespace nextion
diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp
index f8307c6c4b..018f8fe732 100644
--- a/esphome/components/nextion/nextion_commands.cpp
+++ b/esphome/components/nextion/nextion_commands.cpp
@@ -38,7 +38,7 @@ void Nextion::sleep(bool sleep) {
 // Protocol reparse mode
 bool Nextion::set_protocol_reparse_mode(bool active_mode) {
   ESP_LOGV(TAG, "Reparse mode: %s", YESNO(active_mode));
-  this->ignore_is_setup_ = true;  // if not in reparse mode setup will fail, so it should be ignored
+  this->connection_state_.ignore_is_setup_ = true;  // if not in reparse mode setup will fail, so it should be ignored
   bool all_commands_sent = true;
   if (active_mode) {  // Sets active protocol reparse mode
     all_commands_sent &= this->send_command_("recmod=1");
@@ -48,10 +48,10 @@ bool Nextion::set_protocol_reparse_mode(bool active_mode) {
     all_commands_sent &= this->send_command_("recmod=0");  // Sending recmode=0 twice is recommended
     all_commands_sent &= this->send_command_("recmod=0");
   }
-  if (!this->nextion_reports_is_setup_) {  // No need to connect if is already setup
+  if (!this->connection_state_.nextion_reports_is_setup_) {  // No need to connect if is already setup
     all_commands_sent &= this->send_command_("connect");
   }
-  this->ignore_is_setup_ = false;
+  this->connection_state_.ignore_is_setup_ = false;
   return all_commands_sent;
 }
 
@@ -191,7 +191,7 @@ void Nextion::set_backlight_brightness(float brightness) {
 }
 
 void Nextion::set_auto_wake_on_touch(bool auto_wake_on_touch) {
-  this->auto_wake_on_touch_ = auto_wake_on_touch;
+  this->connection_state_.auto_wake_on_touch_ = auto_wake_on_touch;
   this->add_no_result_to_queue_with_set("auto_wake_on_touch", "thup", auto_wake_on_touch ? 1 : 0);
 }
 
diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp
index 6a54abfed4..c47b393f99 100644
--- a/esphome/components/nextion/nextion_upload.cpp
+++ b/esphome/components/nextion/nextion_upload.cpp
@@ -16,8 +16,8 @@ bool Nextion::upload_end_(bool successful) {
   } else {
     ESP_LOGE(TAG, "Upload failed");
 
-    this->is_updating_ = false;
-    this->ignore_is_setup_ = false;
+    this->connection_state_.is_updating_ = false;
+    this->connection_state_.ignore_is_setup_ = false;
 
     uint32_t baud_rate = this->parent_->get_baud_rate();
     if (baud_rate != this->original_baud_rate_) {
diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp
index 6cd03118d2..b0e5d121dd 100644
--- a/esphome/components/nextion/nextion_upload_arduino.cpp
+++ b/esphome/components/nextion/nextion_upload_arduino.cpp
@@ -152,7 +152,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
   ESP_LOGD(TAG, "Exit reparse: %s", YESNO(exit_reparse));
   ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str());
 
-  if (this->is_updating_) {
+  if (this->connection_state_.is_updating_) {
     ESP_LOGW(TAG, "Upload in progress");
     return false;
   }
@@ -162,7 +162,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
     return false;
   }
 
-  this->is_updating_ = true;
+  this->connection_state_.is_updating_ = true;
 
   if (exit_reparse) {
     ESP_LOGD(TAG, "Exit reparse mode");
@@ -203,7 +203,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
   begin_status = http_client.begin(*this->get_wifi_client_(), this->tft_url_.c_str());
 #endif  // USE_ESP8266
   if (!begin_status) {
-    this->is_updating_ = false;
+    this->connection_state_.is_updating_ = false;
     ESP_LOGD(TAG, "Connection failed");
     return false;
   } else {
@@ -254,7 +254,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
 
   // The Nextion will ignore the upload command if it is sleeping
   ESP_LOGV(TAG, "Wake-up");
-  this->ignore_is_setup_ = true;
+  this->connection_state_.ignore_is_setup_ = true;
   this->send_command_("sleep=0");
   this->send_command_("dim=100");
   delay(250);  // NOLINT
diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp
index 14ce46d0a0..78a47f9e2c 100644
--- a/esphome/components/nextion/nextion_upload_idf.cpp
+++ b/esphome/components/nextion/nextion_upload_idf.cpp
@@ -155,7 +155,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
   ESP_LOGD(TAG, "Exit reparse: %s", YESNO(exit_reparse));
   ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str());
 
-  if (this->is_updating_) {
+  if (this->connection_state_.is_updating_) {
     ESP_LOGW(TAG, "Upload in progress");
     return false;
   }
@@ -165,7 +165,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
     return false;
   }
 
-  this->is_updating_ = true;
+  this->connection_state_.is_updating_ = true;
 
   if (exit_reparse) {
     ESP_LOGD(TAG, "Exit reparse mode");
@@ -246,7 +246,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
 
   // The Nextion will ignore the upload command if it is sleeping
   ESP_LOGV(TAG, "Wake-up");
-  this->ignore_is_setup_ = true;
+  this->connection_state_.ignore_is_setup_ = true;
   this->send_command_("sleep=0");
   this->send_command_("dim=100");
   vTaskDelay(pdMS_TO_TICKS(250));  // NOLINT

From fde5f881925adb9c74196f15140be4d7d4373cd2 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Mon, 7 Jul 2025 16:36:34 +1200
Subject: [PATCH 252/293] [inkplate6] Require 240mhz cpu frequency (#9356)

---
 esphome/components/inkplate6/display.py | 15 +++++++++++++++
 tests/components/inkplate6/common.yaml  |  3 +++
 2 files changed, 18 insertions(+)

diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py
index a7d31c0131..063fc8b0aa 100644
--- a/esphome/components/inkplate6/display.py
+++ b/esphome/components/inkplate6/display.py
@@ -1,6 +1,7 @@
 from esphome import pins
 import esphome.codegen as cg
 from esphome.components import display, i2c
+from esphome.components.esp32 import CONF_CPU_FREQUENCY
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_FULL_UPDATE_EVERY,
@@ -13,7 +14,9 @@ from esphome.const import (
     CONF_PAGES,
     CONF_TRANSFORM,
     CONF_WAKEUP_PIN,
+    PLATFORM_ESP32,
 )
+import esphome.final_validate as fv
 
 DEPENDENCIES = ["i2c", "esp32"]
 AUTO_LOAD = ["psram"]
@@ -120,6 +123,18 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
+def _validate_cpu_frequency(config):
+    esp32_config = fv.full_config.get()[PLATFORM_ESP32]
+    if esp32_config[CONF_CPU_FREQUENCY] != "240MHZ":
+        raise cv.Invalid(
+            "Inkplate requires 240MHz CPU frequency (set in esp32 component)"
+        )
+    return config
+
+
+FINAL_VALIDATE_SCHEMA = _validate_cpu_frequency
+
+
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
 
diff --git a/tests/components/inkplate6/common.yaml b/tests/components/inkplate6/common.yaml
index 31b14e6c73..6cb5d055b6 100644
--- a/tests/components/inkplate6/common.yaml
+++ b/tests/components/inkplate6/common.yaml
@@ -3,6 +3,9 @@ i2c:
     scl: 16
     sda: 17
 
+esp32:
+  cpu_frequency: 240MHz
+
 display:
   - platform: inkplate6
     id: inkplate_display

From 83512b88c48ec0e6557285b3f29a2293c4706e9d Mon Sep 17 00:00:00 2001
From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Date: Mon, 7 Jul 2025 01:41:23 -0400
Subject: [PATCH 253/293] [sx126x] Add sx126x component (#8516)

Co-authored-by: J. Nick Koston 
Co-authored-by: J. Nick Koston 
---
 CODEOWNERS                                    |   1 +
 esphome/components/sx126x/__init__.py         | 317 +++++++++++
 esphome/components/sx126x/automation.h        |  62 +++
 .../sx126x/packet_transport/__init__.py       |  26 +
 .../packet_transport/sx126x_transport.cpp     |  26 +
 .../packet_transport/sx126x_transport.h       |  25 +
 esphome/components/sx126x/sx126x.cpp          | 523 ++++++++++++++++++
 esphome/components/sx126x/sx126x.h            | 140 +++++
 esphome/components/sx126x/sx126x_reg.h        | 163 ++++++
 tests/components/sx126x/common.yaml           |  40 ++
 tests/components/sx126x/test.esp32-ard.yaml   |  10 +
 .../components/sx126x/test.esp32-c3-ard.yaml  |  10 +
 .../components/sx126x/test.esp32-c3-idf.yaml  |  10 +
 tests/components/sx126x/test.esp32-idf.yaml   |  10 +
 tests/components/sx126x/test.esp8266-ard.yaml |  10 +
 tests/components/sx126x/test.rp2040-ard.yaml  |  10 +
 16 files changed, 1383 insertions(+)
 create mode 100644 esphome/components/sx126x/__init__.py
 create mode 100644 esphome/components/sx126x/automation.h
 create mode 100644 esphome/components/sx126x/packet_transport/__init__.py
 create mode 100644 esphome/components/sx126x/packet_transport/sx126x_transport.cpp
 create mode 100644 esphome/components/sx126x/packet_transport/sx126x_transport.h
 create mode 100644 esphome/components/sx126x/sx126x.cpp
 create mode 100644 esphome/components/sx126x/sx126x.h
 create mode 100644 esphome/components/sx126x/sx126x_reg.h
 create mode 100644 tests/components/sx126x/common.yaml
 create mode 100644 tests/components/sx126x/test.esp32-ard.yaml
 create mode 100644 tests/components/sx126x/test.esp32-c3-ard.yaml
 create mode 100644 tests/components/sx126x/test.esp32-c3-idf.yaml
 create mode 100644 tests/components/sx126x/test.esp32-idf.yaml
 create mode 100644 tests/components/sx126x/test.esp8266-ard.yaml
 create mode 100644 tests/components/sx126x/test.rp2040-ard.yaml

diff --git a/CODEOWNERS b/CODEOWNERS
index 1a7dc4f227..ca3849eb0d 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -442,6 +442,7 @@ esphome/components/sun/* @OttoWinter
 esphome/components/sun_gtil2/* @Mat931
 esphome/components/switch/* @esphome/core
 esphome/components/switch/binary_sensor/* @ssieb
+esphome/components/sx126x/* @swoboda1337
 esphome/components/sx127x/* @swoboda1337
 esphome/components/syslog/* @clydebarrow
 esphome/components/t6615/* @tylermenezes
diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py
new file mode 100644
index 0000000000..492febe283
--- /dev/null
+++ b/esphome/components/sx126x/__init__.py
@@ -0,0 +1,317 @@
+from esphome import automation, pins
+import esphome.codegen as cg
+from esphome.components import spi
+import esphome.config_validation as cv
+from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID
+from esphome.core import TimePeriod
+
+MULTI_CONF = True
+CODEOWNERS = ["@swoboda1337"]
+DEPENDENCIES = ["spi"]
+
+CONF_SX126X_ID = "sx126x_id"
+
+CONF_BANDWIDTH = "bandwidth"
+CONF_BITRATE = "bitrate"
+CONF_CODING_RATE = "coding_rate"
+CONF_CRC_ENABLE = "crc_enable"
+CONF_DEVIATION = "deviation"
+CONF_DIO1_PIN = "dio1_pin"
+CONF_HW_VERSION = "hw_version"
+CONF_MODULATION = "modulation"
+CONF_ON_PACKET = "on_packet"
+CONF_PA_POWER = "pa_power"
+CONF_PA_RAMP = "pa_ramp"
+CONF_PAYLOAD_LENGTH = "payload_length"
+CONF_PREAMBLE_DETECT = "preamble_detect"
+CONF_PREAMBLE_SIZE = "preamble_size"
+CONF_RST_PIN = "rst_pin"
+CONF_RX_START = "rx_start"
+CONF_RF_SWITCH = "rf_switch"
+CONF_SHAPING = "shaping"
+CONF_SPREADING_FACTOR = "spreading_factor"
+CONF_SYNC_VALUE = "sync_value"
+CONF_TCXO_VOLTAGE = "tcxo_voltage"
+CONF_TCXO_DELAY = "tcxo_delay"
+
+sx126x_ns = cg.esphome_ns.namespace("sx126x")
+SX126x = sx126x_ns.class_("SX126x", cg.Component, spi.SPIDevice)
+SX126xListener = sx126x_ns.class_("SX126xListener")
+SX126xBw = sx126x_ns.enum("SX126xBw")
+SX126xPacketType = sx126x_ns.enum("SX126xPacketType")
+SX126xTcxoCtrl = sx126x_ns.enum("SX126xTcxoCtrl")
+SX126xRampTime = sx126x_ns.enum("SX126xRampTime")
+SX126xPulseShape = sx126x_ns.enum("SX126xPulseShape")
+SX126xLoraCr = sx126x_ns.enum("SX126xLoraCr")
+
+BW = {
+    "4_8kHz": SX126xBw.SX126X_BW_4800,
+    "5_8kHz": SX126xBw.SX126X_BW_5800,
+    "7_3kHz": SX126xBw.SX126X_BW_7300,
+    "9_7kHz": SX126xBw.SX126X_BW_9700,
+    "11_7kHz": SX126xBw.SX126X_BW_11700,
+    "14_6kHz": SX126xBw.SX126X_BW_14600,
+    "19_5kHz": SX126xBw.SX126X_BW_19500,
+    "23_4kHz": SX126xBw.SX126X_BW_23400,
+    "29_3kHz": SX126xBw.SX126X_BW_29300,
+    "39_0kHz": SX126xBw.SX126X_BW_39000,
+    "46_9kHz": SX126xBw.SX126X_BW_46900,
+    "58_6kHz": SX126xBw.SX126X_BW_58600,
+    "78_2kHz": SX126xBw.SX126X_BW_78200,
+    "93_8kHz": SX126xBw.SX126X_BW_93800,
+    "117_3kHz": SX126xBw.SX126X_BW_117300,
+    "156_2kHz": SX126xBw.SX126X_BW_156200,
+    "187_2kHz": SX126xBw.SX126X_BW_187200,
+    "234_3kHz": SX126xBw.SX126X_BW_234300,
+    "312_0kHz": SX126xBw.SX126X_BW_312000,
+    "373_6kHz": SX126xBw.SX126X_BW_373600,
+    "467_0kHz": SX126xBw.SX126X_BW_467000,
+    "7_8kHz": SX126xBw.SX126X_BW_7810,
+    "10_4kHz": SX126xBw.SX126X_BW_10420,
+    "15_6kHz": SX126xBw.SX126X_BW_15630,
+    "20_8kHz": SX126xBw.SX126X_BW_20830,
+    "31_3kHz": SX126xBw.SX126X_BW_31250,
+    "41_7kHz": SX126xBw.SX126X_BW_41670,
+    "62_5kHz": SX126xBw.SX126X_BW_62500,
+    "125_0kHz": SX126xBw.SX126X_BW_125000,
+    "250_0kHz": SX126xBw.SX126X_BW_250000,
+    "500_0kHz": SX126xBw.SX126X_BW_500000,
+}
+
+CODING_RATE = {
+    "CR_4_5": SX126xLoraCr.LORA_CR_4_5,
+    "CR_4_6": SX126xLoraCr.LORA_CR_4_6,
+    "CR_4_7": SX126xLoraCr.LORA_CR_4_7,
+    "CR_4_8": SX126xLoraCr.LORA_CR_4_8,
+}
+
+MOD = {
+    "LORA": SX126xPacketType.PACKET_TYPE_LORA,
+    "FSK": SX126xPacketType.PACKET_TYPE_GFSK,
+}
+
+TCXO_VOLTAGE = {
+    "1_6V": SX126xTcxoCtrl.TCXO_CTRL_1_6V,
+    "1_7V": SX126xTcxoCtrl.TCXO_CTRL_1_7V,
+    "1_8V": SX126xTcxoCtrl.TCXO_CTRL_1_8V,
+    "2_2V": SX126xTcxoCtrl.TCXO_CTRL_2_2V,
+    "2_4V": SX126xTcxoCtrl.TCXO_CTRL_2_4V,
+    "2_7V": SX126xTcxoCtrl.TCXO_CTRL_2_7V,
+    "3_0V": SX126xTcxoCtrl.TCXO_CTRL_3_0V,
+    "3_3V": SX126xTcxoCtrl.TCXO_CTRL_3_3V,
+    "NONE": SX126xTcxoCtrl.TCXO_CTRL_NONE,
+}
+
+RAMP = {
+    "10us": SX126xRampTime.PA_RAMP_10,
+    "20us": SX126xRampTime.PA_RAMP_20,
+    "40us": SX126xRampTime.PA_RAMP_40,
+    "80us": SX126xRampTime.PA_RAMP_80,
+    "200us": SX126xRampTime.PA_RAMP_200,
+    "800us": SX126xRampTime.PA_RAMP_800,
+    "1700us": SX126xRampTime.PA_RAMP_1700,
+    "3400us": SX126xRampTime.PA_RAMP_3400,
+}
+
+SHAPING = {
+    "GAUSSIAN_BT_0_3": SX126xPulseShape.GAUSSIAN_BT_0_3,
+    "GAUSSIAN_BT_0_5": SX126xPulseShape.GAUSSIAN_BT_0_5,
+    "GAUSSIAN_BT_0_7": SX126xPulseShape.GAUSSIAN_BT_0_7,
+    "GAUSSIAN_BT_1_0": SX126xPulseShape.GAUSSIAN_BT_1_0,
+    "NONE": SX126xPulseShape.NO_FILTER,
+}
+
+RunImageCalAction = sx126x_ns.class_(
+    "RunImageCalAction", automation.Action, cg.Parented.template(SX126x)
+)
+SendPacketAction = sx126x_ns.class_(
+    "SendPacketAction", automation.Action, cg.Parented.template(SX126x)
+)
+SetModeTxAction = sx126x_ns.class_(
+    "SetModeTxAction", automation.Action, cg.Parented.template(SX126x)
+)
+SetModeRxAction = sx126x_ns.class_(
+    "SetModeRxAction", automation.Action, cg.Parented.template(SX126x)
+)
+SetModeSleepAction = sx126x_ns.class_(
+    "SetModeSleepAction", automation.Action, cg.Parented.template(SX126x)
+)
+SetModeStandbyAction = sx126x_ns.class_(
+    "SetModeStandbyAction", automation.Action, cg.Parented.template(SX126x)
+)
+
+
+def validate_raw_data(value):
+    if isinstance(value, str):
+        return value.encode("utf-8")
+    if isinstance(value, list):
+        return cv.Schema([cv.hex_uint8_t])(value)
+    raise cv.Invalid(
+        "data must either be a string wrapped in quotes or a list of bytes"
+    )
+
+
+def validate_config(config):
+    lora_bws = [
+        "7_8kHz",
+        "10_4kHz",
+        "15_6kHz",
+        "20_8kHz",
+        "31_3kHz",
+        "41_7kHz",
+        "62_5kHz",
+        "125_0kHz",
+        "250_0kHz",
+        "500_0kHz",
+    ]
+    if config[CONF_MODULATION] == "LORA":
+        if config[CONF_BANDWIDTH] not in lora_bws:
+            raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with LORA")
+        if config[CONF_PREAMBLE_SIZE] > 0 and config[CONF_PREAMBLE_SIZE] < 6:
+            raise cv.Invalid("Minimum preamble size is 6 with LORA")
+        if config[CONF_SPREADING_FACTOR] == 6 and config[CONF_PAYLOAD_LENGTH] == 0:
+            raise cv.Invalid("Payload length must be set when spreading factor is 6")
+    else:
+        if config[CONF_BANDWIDTH] in lora_bws:
+            raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with FSK")
+        if config[CONF_PREAMBLE_DETECT] > len(config[CONF_SYNC_VALUE]):
+            raise cv.Invalid("Preamble detection length must be <= sync value length")
+    return config
+
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(SX126x),
+            cv.Optional(CONF_BANDWIDTH, default="125_0kHz"): cv.enum(BW),
+            cv.Optional(CONF_BITRATE, default=4800): cv.int_range(min=600, max=300000),
+            cv.Required(CONF_BUSY_PIN): pins.internal_gpio_input_pin_schema,
+            cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE),
+            cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
+            cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000),
+            cv.Required(CONF_DIO1_PIN): pins.internal_gpio_input_pin_schema,
+            cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000),
+            cv.Required(CONF_HW_VERSION): cv.one_of(
+                "sx1261", "sx1262", "sx1268", "llcc68", lower=True
+            ),
+            cv.Required(CONF_MODULATION): cv.enum(MOD),
+            cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
+            cv.Optional(CONF_PA_POWER, default=17): cv.int_range(min=-3, max=22),
+            cv.Optional(CONF_PA_RAMP, default="40us"): cv.enum(RAMP),
+            cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=256),
+            cv.Optional(CONF_PREAMBLE_DETECT, default=2): cv.int_range(min=0, max=4),
+            cv.Required(CONF_PREAMBLE_SIZE): cv.int_range(min=1, max=65535),
+            cv.Required(CONF_RST_PIN): pins.internal_gpio_output_pin_schema,
+            cv.Optional(CONF_RX_START, default=True): cv.boolean,
+            cv.Required(CONF_RF_SWITCH): cv.boolean,
+            cv.Optional(CONF_SHAPING, default="NONE"): cv.enum(SHAPING),
+            cv.Optional(CONF_SPREADING_FACTOR, default=7): cv.int_range(min=6, max=12),
+            cv.Optional(CONF_SYNC_VALUE, default=[]): cv.ensure_list(cv.hex_uint8_t),
+            cv.Optional(CONF_TCXO_VOLTAGE, default="NONE"): cv.enum(TCXO_VOLTAGE),
+            cv.Optional(CONF_TCXO_DELAY, default="5ms"): cv.All(
+                cv.positive_time_period_microseconds,
+                cv.Range(max=TimePeriod(microseconds=262144000)),
+            ),
+        },
+    )
+    .extend(cv.COMPONENT_SCHEMA)
+    .extend(spi.spi_device_schema(True, 8e6, "mode0"))
+    .add_extra(validate_config)
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await spi.register_spi_device(var, config)
+    if CONF_ON_PACKET in config:
+        await automation.build_automation(
+            var.get_packet_trigger(),
+            [
+                (cg.std_vector.template(cg.uint8), "x"),
+                (cg.float_, "rssi"),
+                (cg.float_, "snr"),
+            ],
+            config[CONF_ON_PACKET],
+        )
+    if CONF_DIO1_PIN in config:
+        dio1_pin = await cg.gpio_pin_expression(config[CONF_DIO1_PIN])
+        cg.add(var.set_dio1_pin(dio1_pin))
+    rst_pin = await cg.gpio_pin_expression(config[CONF_RST_PIN])
+    cg.add(var.set_rst_pin(rst_pin))
+    busy_pin = await cg.gpio_pin_expression(config[CONF_BUSY_PIN])
+    cg.add(var.set_busy_pin(busy_pin))
+    cg.add(var.set_bandwidth(config[CONF_BANDWIDTH]))
+    cg.add(var.set_frequency(config[CONF_FREQUENCY]))
+    cg.add(var.set_hw_version(config[CONF_HW_VERSION]))
+    cg.add(var.set_deviation(config[CONF_DEVIATION]))
+    cg.add(var.set_modulation(config[CONF_MODULATION]))
+    cg.add(var.set_pa_ramp(config[CONF_PA_RAMP]))
+    cg.add(var.set_pa_power(config[CONF_PA_POWER]))
+    cg.add(var.set_shaping(config[CONF_SHAPING]))
+    cg.add(var.set_bitrate(config[CONF_BITRATE]))
+    cg.add(var.set_crc_enable(config[CONF_CRC_ENABLE]))
+    cg.add(var.set_payload_length(config[CONF_PAYLOAD_LENGTH]))
+    cg.add(var.set_preamble_size(config[CONF_PREAMBLE_SIZE]))
+    cg.add(var.set_preamble_detect(config[CONF_PREAMBLE_DETECT]))
+    cg.add(var.set_coding_rate(config[CONF_CODING_RATE]))
+    cg.add(var.set_spreading_factor(config[CONF_SPREADING_FACTOR]))
+    cg.add(var.set_sync_value(config[CONF_SYNC_VALUE]))
+    cg.add(var.set_rx_start(config[CONF_RX_START]))
+    cg.add(var.set_rf_switch(config[CONF_RF_SWITCH]))
+    cg.add(var.set_tcxo_voltage(config[CONF_TCXO_VOLTAGE]))
+    cg.add(var.set_tcxo_delay(config[CONF_TCXO_DELAY]))
+
+
+NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id(
+    {
+        cv.GenerateID(): cv.use_id(SX126x),
+    }
+)
+
+
+@automation.register_action(
+    "sx126x.run_image_cal", RunImageCalAction, NO_ARGS_ACTION_SCHEMA
+)
+@automation.register_action(
+    "sx126x.set_mode_tx", SetModeTxAction, NO_ARGS_ACTION_SCHEMA
+)
+@automation.register_action(
+    "sx126x.set_mode_rx", SetModeRxAction, NO_ARGS_ACTION_SCHEMA
+)
+@automation.register_action(
+    "sx126x.set_mode_sleep", SetModeSleepAction, NO_ARGS_ACTION_SCHEMA
+)
+@automation.register_action(
+    "sx126x.set_mode_standby", SetModeStandbyAction, NO_ARGS_ACTION_SCHEMA
+)
+async def no_args_action_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    await cg.register_parented(var, config[CONF_ID])
+    return var
+
+
+SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
+    {
+        cv.GenerateID(): cv.use_id(SX126x),
+        cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
+    },
+    key=CONF_DATA,
+)
+
+
+@automation.register_action(
+    "sx126x.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA
+)
+async def send_packet_action_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    await cg.register_parented(var, config[CONF_ID])
+    data = config[CONF_DATA]
+    if isinstance(data, bytes):
+        data = list(data)
+    if cg.is_template(data):
+        templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
+        cg.add(var.set_data_template(templ))
+    else:
+        cg.add(var.set_data_static(data))
+    return var
diff --git a/esphome/components/sx126x/automation.h b/esphome/components/sx126x/automation.h
new file mode 100644
index 0000000000..520ef99718
--- /dev/null
+++ b/esphome/components/sx126x/automation.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/automation.h"
+#include "esphome/components/sx126x/sx126x.h"
+
+namespace esphome {
+namespace sx126x {
+
+template class RunImageCalAction : public Action, public Parented {
+ public:
+  void play(Ts... x) override { this->parent_->run_image_cal(); }
+};
+
+template class SendPacketAction : public Action, public Parented {
+ public:
+  void set_data_template(std::function(Ts...)> func) {
+    this->data_func_ = func;
+    this->static_ = false;
+  }
+
+  void set_data_static(const std::vector &data) {
+    this->data_static_ = data;
+    this->static_ = true;
+  }
+
+  void play(Ts... x) override {
+    if (this->static_) {
+      this->parent_->transmit_packet(this->data_static_);
+    } else {
+      this->parent_->transmit_packet(this->data_func_(x...));
+    }
+  }
+
+ protected:
+  bool static_{false};
+  std::function(Ts...)> data_func_{};
+  std::vector data_static_{};
+};
+
+template class SetModeTxAction : public Action, public Parented {
+ public:
+  void play(Ts... x) override { this->parent_->set_mode_tx(); }
+};
+
+template class SetModeRxAction : public Action, public Parented {
+ public:
+  void play(Ts... x) override { this->parent_->set_mode_rx(); }
+};
+
+template class SetModeSleepAction : public Action, public Parented {
+ public:
+  void play(Ts... x) override { this->parent_->set_mode_sleep(); }
+};
+
+template class SetModeStandbyAction : public Action, public Parented {
+ public:
+  void play(Ts... x) override { this->parent_->set_mode_standby(STDBY_XOSC); }
+};
+
+}  // namespace sx126x
+}  // namespace esphome
diff --git a/esphome/components/sx126x/packet_transport/__init__.py b/esphome/components/sx126x/packet_transport/__init__.py
new file mode 100644
index 0000000000..4d79b23ac1
--- /dev/null
+++ b/esphome/components/sx126x/packet_transport/__init__.py
@@ -0,0 +1,26 @@
+import esphome.codegen as cg
+from esphome.components.packet_transport import (
+    PacketTransport,
+    new_packet_transport,
+    transport_schema,
+)
+import esphome.config_validation as cv
+from esphome.cpp_types import PollingComponent
+
+from .. import CONF_SX126X_ID, SX126x, SX126xListener, sx126x_ns
+
+SX126xTransport = sx126x_ns.class_(
+    "SX126xTransport", PacketTransport, PollingComponent, SX126xListener
+)
+
+CONFIG_SCHEMA = transport_schema(SX126xTransport).extend(
+    {
+        cv.GenerateID(CONF_SX126X_ID): cv.use_id(SX126x),
+    }
+)
+
+
+async def to_code(config):
+    var, _ = await new_packet_transport(config)
+    sx126x = await cg.get_variable(config[CONF_SX126X_ID])
+    cg.add(var.set_parent(sx126x))
diff --git a/esphome/components/sx126x/packet_transport/sx126x_transport.cpp b/esphome/components/sx126x/packet_transport/sx126x_transport.cpp
new file mode 100644
index 0000000000..2cfc4b700e
--- /dev/null
+++ b/esphome/components/sx126x/packet_transport/sx126x_transport.cpp
@@ -0,0 +1,26 @@
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+#include "sx126x_transport.h"
+
+namespace esphome {
+namespace sx126x {
+
+static const char *const TAG = "sx126x_transport";
+
+void SX126xTransport::setup() {
+  PacketTransport::setup();
+  this->parent_->register_listener(this);
+}
+
+void SX126xTransport::update() {
+  PacketTransport::update();
+  this->updated_ = true;
+  this->resend_data_ = true;
+}
+
+void SX126xTransport::send_packet(const std::vector &buf) const { this->parent_->transmit_packet(buf); }
+
+void SX126xTransport::on_packet(const std::vector &packet, float rssi, float snr) { this->process_(packet); }
+
+}  // namespace sx126x
+}  // namespace esphome
diff --git a/esphome/components/sx126x/packet_transport/sx126x_transport.h b/esphome/components/sx126x/packet_transport/sx126x_transport.h
new file mode 100644
index 0000000000..755d30417d
--- /dev/null
+++ b/esphome/components/sx126x/packet_transport/sx126x_transport.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sx126x/sx126x.h"
+#include "esphome/components/packet_transport/packet_transport.h"
+#include 
+
+namespace esphome {
+namespace sx126x {
+
+class SX126xTransport : public packet_transport::PacketTransport, public Parented, public SX126xListener {
+ public:
+  void setup() override;
+  void update() override;
+  void on_packet(const std::vector &packet, float rssi, float snr) override;
+  float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
+
+ protected:
+  void send_packet(const std::vector &buf) const override;
+  bool should_send() override { return true; }
+  size_t get_max_packet_size() override { return this->parent_->get_max_packet_size(); }
+};
+
+}  // namespace sx126x
+}  // namespace esphome
diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp
new file mode 100644
index 0000000000..b1c81b324a
--- /dev/null
+++ b/esphome/components/sx126x/sx126x.cpp
@@ -0,0 +1,523 @@
+#include "sx126x.h"
+#include "esphome/core/hal.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace sx126x {
+
+static const char *const TAG = "sx126x";
+static const uint16_t RAMP[8] = {10, 20, 40, 80, 200, 800, 1700, 3400};
+static const uint32_t BW_HZ[31] = {4800,  5800,  7300,  9700,   11700,  14600,  19500,  23400,  29300,  39000,  46900,
+                                   58600, 78200, 93800, 117300, 156200, 187200, 234300, 312000, 373600, 467000, 7810,
+                                   10420, 15630, 20830, 31250,  41670,  62500,  125000, 250000, 500000};
+static const uint8_t BW_LORA[10] = {LORA_BW_7810,  LORA_BW_10420, LORA_BW_15630,  LORA_BW_20830,  LORA_BW_31250,
+                                    LORA_BW_41670, LORA_BW_62500, LORA_BW_125000, LORA_BW_250000, LORA_BW_500000};
+static const uint8_t BW_FSK[21] = {
+    FSK_BW_4800,   FSK_BW_5800,   FSK_BW_7300,   FSK_BW_9700,   FSK_BW_11700,  FSK_BW_14600,  FSK_BW_19500,
+    FSK_BW_23400,  FSK_BW_29300,  FSK_BW_39000,  FSK_BW_46900,  FSK_BW_58600,  FSK_BW_78200,  FSK_BW_93800,
+    FSK_BW_117300, FSK_BW_156200, FSK_BW_187200, FSK_BW_234300, FSK_BW_312000, FSK_BW_373600, FSK_BW_467000};
+
+static constexpr uint32_t RESET_DELAY_HIGH_US = 5000;
+static constexpr uint32_t RESET_DELAY_LOW_US = 2000;
+static constexpr uint32_t SWITCHING_DELAY_US = 1;
+static constexpr uint32_t TRANSMIT_TIMEOUT_MS = 4000;
+static constexpr uint32_t BUSY_TIMEOUT_MS = 20;
+
+// OCP (Over Current Protection) values
+static constexpr uint8_t OCP_80MA = 0x18;   // 80 mA max current
+static constexpr uint8_t OCP_140MA = 0x38;  // 140 mA max current
+
+// LoRa low data rate optimization threshold
+static constexpr float LOW_DATA_RATE_OPTIMIZE_THRESHOLD = 16.38f;  // 16.38 ms
+
+uint8_t SX126x::read_fifo_(uint8_t offset, std::vector &packet) {
+  this->wait_busy_();
+  this->enable();
+  this->transfer_byte(RADIO_READ_BUFFER);
+  this->transfer_byte(offset);
+  uint8_t status = this->transfer_byte(0x00);
+  for (uint8_t &byte : packet) {
+    byte = this->transfer_byte(0x00);
+  }
+  this->disable();
+  return status;
+}
+
+void SX126x::write_fifo_(uint8_t offset, const std::vector &packet) {
+  this->wait_busy_();
+  this->enable();
+  this->transfer_byte(RADIO_WRITE_BUFFER);
+  this->transfer_byte(offset);
+  for (const uint8_t &byte : packet) {
+    this->transfer_byte(byte);
+  }
+  this->disable();
+  delayMicroseconds(SWITCHING_DELAY_US);
+}
+
+uint8_t SX126x::read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) {
+  this->wait_busy_();
+  this->enable();
+  this->transfer_byte(opcode);
+  uint8_t status = this->transfer_byte(0x00);
+  for (int32_t i = 0; i < size; i++) {
+    data[i] = this->transfer_byte(0x00);
+  }
+  this->disable();
+  return status;
+}
+
+void SX126x::write_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) {
+  this->wait_busy_();
+  this->enable();
+  this->transfer_byte(opcode);
+  for (int32_t i = 0; i < size; i++) {
+    this->transfer_byte(data[i]);
+  }
+  this->disable();
+  delayMicroseconds(SWITCHING_DELAY_US);
+}
+
+void SX126x::read_register_(uint16_t reg, uint8_t *data, uint8_t size) {
+  this->wait_busy_();
+  this->enable();
+  this->write_byte(RADIO_READ_REGISTER);
+  this->write_byte((reg >> 8) & 0xFF);
+  this->write_byte((reg >> 0) & 0xFF);
+  this->write_byte(0x00);
+  for (int32_t i = 0; i < size; i++) {
+    data[i] = this->transfer_byte(0x00);
+  }
+  this->disable();
+}
+
+void SX126x::write_register_(uint16_t reg, uint8_t *data, uint8_t size) {
+  this->wait_busy_();
+  this->enable();
+  this->write_byte(RADIO_WRITE_REGISTER);
+  this->write_byte((reg >> 8) & 0xFF);
+  this->write_byte((reg >> 0) & 0xFF);
+  for (int32_t i = 0; i < size; i++) {
+    this->transfer_byte(data[i]);
+  }
+  this->disable();
+  delayMicroseconds(SWITCHING_DELAY_US);
+}
+
+void SX126x::setup() {
+  ESP_LOGCONFIG(TAG, "Running setup");
+
+  // setup pins
+  this->busy_pin_->setup();
+  this->rst_pin_->setup();
+  this->dio1_pin_->setup();
+
+  // start spi
+  this->spi_setup();
+
+  // configure rf
+  this->configure();
+}
+
+void SX126x::configure() {
+  uint8_t buf[8];
+
+  // toggle chip reset
+  this->rst_pin_->digital_write(true);
+  delayMicroseconds(RESET_DELAY_HIGH_US);
+  this->rst_pin_->digital_write(false);
+  delayMicroseconds(RESET_DELAY_LOW_US);
+  this->rst_pin_->digital_write(true);
+  delayMicroseconds(RESET_DELAY_HIGH_US);
+
+  // wakeup
+  this->read_opcode_(RADIO_GET_STATUS, nullptr, 0);
+
+  // config tcxo
+  if (this->tcxo_voltage_ != TCXO_CTRL_NONE) {
+    uint32_t delay = this->tcxo_delay_ >> 6;
+    buf[0] = this->tcxo_voltage_;
+    buf[1] = (delay >> 16) & 0xFF;
+    buf[2] = (delay >> 8) & 0xFF;
+    buf[3] = (delay >> 0) & 0xFF;
+    this->write_opcode_(RADIO_SET_TCXOMODE, buf, 4);
+    buf[0] = 0x7F;
+    this->write_opcode_(RADIO_CALIBRATE, buf, 1);
+  }
+
+  // clear errors
+  buf[0] = 0x00;
+  buf[1] = 0x00;
+  this->write_opcode_(RADIO_CLR_ERROR, buf, 2);
+
+  // rf switch
+  if (this->rf_switch_) {
+    buf[0] = 0x01;
+    this->write_opcode_(RADIO_SET_RFSWITCHMODE, buf, 1);
+  }
+
+  // check silicon version to make sure hw is ok
+  this->read_register_(REG_VERSION_STRING, (uint8_t *) this->version_, 16);
+  if (strncmp(this->version_, "SX126", 5) != 0 && strncmp(this->version_, "LLCC68", 6) != 0) {
+    this->mark_failed();
+    return;
+  }
+
+  // setup packet type
+  buf[0] = this->modulation_;
+  this->write_opcode_(RADIO_SET_PACKETTYPE, buf, 1);
+
+  // calibrate image
+  this->run_image_cal();
+
+  // set frequency
+  uint64_t freq = ((uint64_t) this->frequency_ << 25) / XTAL_FREQ;
+  buf[0] = (uint8_t) ((freq >> 24) & 0xFF);
+  buf[1] = (uint8_t) ((freq >> 16) & 0xFF);
+  buf[2] = (uint8_t) ((freq >> 8) & 0xFF);
+  buf[3] = (uint8_t) (freq & 0xFF);
+  this->write_opcode_(RADIO_SET_RFFREQUENCY, buf, 4);
+
+  // configure pa
+  int8_t pa_power = this->pa_power_;
+  if (this->hw_version_ == "sx1261") {
+    // the following values were taken from section 13.1.14.1 table 13-21
+    // in rev 2.1 of the datasheet
+    if (pa_power == 15) {
+      uint8_t cfg[4] = {0x06, 0x00, 0x01, 0x01};
+      this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
+    } else {
+      uint8_t cfg[4] = {0x04, 0x00, 0x01, 0x01};
+      this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
+    }
+    pa_power = std::max(pa_power, (int8_t) -3);
+    pa_power = std::min(pa_power, (int8_t) 14);
+    buf[0] = OCP_80MA;
+    this->write_register_(REG_OCP, buf, 1);
+  } else {
+    // the following values were taken from section 13.1.14.1 table 13-21
+    // in rev 2.1 of the datasheet
+    uint8_t cfg[4] = {0x04, 0x07, 0x00, 0x01};
+    this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
+    pa_power = std::max(pa_power, (int8_t) -3);
+    pa_power = std::min(pa_power, (int8_t) 22);
+    buf[0] = OCP_140MA;
+    this->write_register_(REG_OCP, buf, 1);
+  }
+  buf[0] = pa_power;
+  buf[1] = this->pa_ramp_;
+  this->write_opcode_(RADIO_SET_TXPARAMS, buf, 2);
+
+  // configure modem
+  if (this->modulation_ == PACKET_TYPE_LORA) {
+    // set modulation params
+    float duration = 1000.0f * std::pow(2, this->spreading_factor_) / BW_HZ[this->bandwidth_];
+    buf[0] = this->spreading_factor_;
+    buf[1] = BW_LORA[this->bandwidth_ - SX126X_BW_7810];
+    buf[2] = this->coding_rate_;
+    buf[3] = (duration > LOW_DATA_RATE_OPTIMIZE_THRESHOLD) ? 0x01 : 0x00;
+    this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 4);
+
+    // set packet params and sync word
+    this->set_packet_params_(this->payload_length_);
+    if (this->sync_value_.size() == 2) {
+      this->write_register_(REG_LORA_SYNCWORD, this->sync_value_.data(), this->sync_value_.size());
+    }
+  } else {
+    // set modulation params
+    uint32_t bitrate = ((uint64_t) XTAL_FREQ * 32) / this->bitrate_;
+    uint32_t fdev = ((uint64_t) this->deviation_ << 25) / XTAL_FREQ;
+    buf[0] = (bitrate >> 16) & 0xFF;
+    buf[1] = (bitrate >> 8) & 0xFF;
+    buf[2] = (bitrate >> 0) & 0xFF;
+    buf[3] = this->shaping_;
+    buf[4] = BW_FSK[this->bandwidth_ - SX126X_BW_4800];
+    buf[5] = (fdev >> 16) & 0xFF;
+    buf[6] = (fdev >> 8) & 0xFF;
+    buf[7] = (fdev >> 0) & 0xFF;
+    this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 8);
+
+    // set packet params and sync word
+    this->set_packet_params_(this->payload_length_);
+    if (!this->sync_value_.empty()) {
+      this->write_register_(REG_GFSK_SYNCWORD, this->sync_value_.data(), this->sync_value_.size());
+    }
+  }
+
+  // switch to rx or sleep
+  if (this->rx_start_) {
+    this->set_mode_rx();
+  } else {
+    this->set_mode_sleep();
+  }
+}
+
+size_t SX126x::get_max_packet_size() {
+  if (this->payload_length_ > 0) {
+    return this->payload_length_;
+  }
+  return 255;
+}
+
+void SX126x::set_packet_params_(uint8_t payload_length) {
+  uint8_t buf[9];
+  if (this->modulation_ == PACKET_TYPE_LORA) {
+    buf[0] = (this->preamble_size_ >> 8) & 0xFF;
+    buf[1] = (this->preamble_size_ >> 0) & 0xFF;
+    buf[2] = (this->payload_length_ > 0) ? 0x01 : 0x00;
+    buf[3] = payload_length;
+    buf[4] = (this->crc_enable_) ? 0x01 : 0x00;
+    buf[5] = 0x00;
+    this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 6);
+  } else {
+    uint16_t preamble_size = this->preamble_size_ * 8;
+    buf[0] = (preamble_size >> 8) & 0xFF;
+    buf[1] = (preamble_size >> 0) & 0xFF;
+    buf[2] = (this->preamble_detect_ > 0) ? ((this->preamble_detect_ - 1) | 0x04) : 0x00;
+    buf[3] = this->sync_value_.size() * 8;
+    buf[4] = 0x00;
+    buf[5] = 0x00;
+    buf[6] = payload_length;
+    buf[7] = this->crc_enable_ ? 0x06 : 0x01;
+    buf[8] = 0x00;
+    this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 9);
+  }
+}
+
+SX126xError SX126x::transmit_packet(const std::vector &packet) {
+  if (this->payload_length_ > 0 && this->payload_length_ != packet.size()) {
+    ESP_LOGE(TAG, "Packet size does not match config");
+    return SX126xError::INVALID_PARAMS;
+  }
+  if (packet.empty() || packet.size() > this->get_max_packet_size()) {
+    ESP_LOGE(TAG, "Packet size out of range");
+    return SX126xError::INVALID_PARAMS;
+  }
+
+  SX126xError ret = SX126xError::NONE;
+  this->set_mode_standby(STDBY_XOSC);
+  if (this->payload_length_ == 0) {
+    this->set_packet_params_(packet.size());
+  }
+  this->write_fifo_(0x00, packet);
+  this->set_mode_tx();
+
+  // wait until transmit completes, typically the delay will be less than 100 ms
+  uint32_t start = millis();
+  while (!this->dio1_pin_->digital_read()) {
+    if (millis() - start > TRANSMIT_TIMEOUT_MS) {
+      ESP_LOGE(TAG, "Transmit packet failure");
+      ret = SX126xError::TIMEOUT;
+      break;
+    }
+  }
+
+  uint8_t buf[2];
+  buf[0] = 0xFF;
+  buf[1] = 0xFF;
+  this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2);
+  if (this->rx_start_) {
+    this->set_mode_rx();
+  } else {
+    this->set_mode_sleep();
+  }
+  return ret;
+}
+
+void SX126x::call_listeners_(const std::vector &packet, float rssi, float snr) {
+  for (auto &listener : this->listeners_) {
+    listener->on_packet(packet, rssi, snr);
+  }
+  this->packet_trigger_->trigger(packet, rssi, snr);
+}
+
+void SX126x::loop() {
+  if (!this->dio1_pin_->digital_read()) {
+    return;
+  }
+
+  uint16_t status;
+  uint8_t buf[3];
+  uint8_t rssi;
+  int8_t snr;
+  this->read_opcode_(RADIO_GET_IRQSTATUS, buf, 2);
+  this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2);
+  status = (buf[0] << 8) | buf[1];
+  if ((status & IRQ_RX_DONE) == IRQ_RX_DONE) {
+    if ((status & IRQ_CRC_ERROR) != IRQ_CRC_ERROR) {
+      this->read_opcode_(RADIO_GET_PACKETSTATUS, buf, 3);
+      if (this->modulation_ == PACKET_TYPE_LORA) {
+        rssi = buf[0];
+        snr = buf[1];
+      } else {
+        rssi = buf[2];
+        snr = 0;
+      }
+      this->read_opcode_(RADIO_GET_RXBUFFERSTATUS, buf, 2);
+      this->packet_.resize(buf[0]);
+      this->read_fifo_(buf[1], this->packet_);
+      this->call_listeners_(this->packet_, (float) rssi / -2.0f, (float) snr / 4.0f);
+    }
+  }
+}
+
+void SX126x::run_image_cal() {
+  // the following values were taken from section 9.2.1 table 9-2
+  // in rev 2.1 of the datasheet
+  uint8_t buf[2] = {0, 0};
+  if (this->frequency_ > 900000000) {
+    buf[0] = 0xE1;
+    buf[1] = 0xE9;
+  } else if (this->frequency_ > 850000000) {
+    buf[0] = 0xD7;
+    buf[1] = 0xD8;
+  } else if (this->frequency_ > 770000000) {
+    buf[0] = 0xC1;
+    buf[1] = 0xC5;
+  } else if (this->frequency_ > 460000000) {
+    buf[0] = 0x75;
+    buf[1] = 0x81;
+  } else if (this->frequency_ > 425000000) {
+    buf[0] = 0x6B;
+    buf[1] = 0x6F;
+  }
+  if (buf[0] > 0 && buf[1] > 0) {
+    this->write_opcode_(RADIO_CALIBRATEIMAGE, buf, 2);
+  }
+}
+
+void SX126x::set_mode_rx() {
+  uint8_t buf[8];
+
+  // configure irq params
+  uint16_t irq = IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT | IRQ_CRC_ERROR;
+  buf[0] = (irq >> 8) & 0xFF;
+  buf[1] = (irq >> 0) & 0xFF;
+  buf[2] = (irq >> 8) & 0xFF;
+  buf[3] = (irq >> 0) & 0xFF;
+  buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF;
+  buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF;
+  buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF;
+  buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF;
+  this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8);
+
+  // set timeout to 0
+  buf[0] = 0x00;
+  this->write_opcode_(RADIO_SET_LORASYMBTIMEOUT, buf, 1);
+
+  // switch to continuous mode rx
+  buf[0] = 0xFF;
+  buf[1] = 0xFF;
+  buf[2] = 0xFF;
+  this->write_opcode_(RADIO_SET_RX, buf, 3);
+}
+
+void SX126x::set_mode_tx() {
+  uint8_t buf[8];
+
+  // configure irq params
+  uint16_t irq = IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT;
+  buf[0] = (irq >> 8) & 0xFF;
+  buf[1] = (irq >> 0) & 0xFF;
+  buf[2] = (irq >> 8) & 0xFF;
+  buf[3] = (irq >> 0) & 0xFF;
+  buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF;
+  buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF;
+  buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF;
+  buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF;
+  this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8);
+
+  // switch to single mode tx
+  buf[0] = 0x00;
+  buf[1] = 0x00;
+  buf[2] = 0x00;
+  this->write_opcode_(RADIO_SET_TX, buf, 3);
+}
+
+void SX126x::set_mode_sleep() {
+  uint8_t buf[1];
+  buf[0] = 0x05;
+  this->write_opcode_(RADIO_SET_SLEEP, buf, 1);
+}
+
+void SX126x::set_mode_standby(SX126xStandbyMode mode) {
+  uint8_t buf[1];
+  buf[0] = mode;
+  this->write_opcode_(RADIO_SET_STANDBY, buf, 1);
+}
+
+void SX126x::wait_busy_() {
+  // wait if the device is busy, the maximum delay is only be a few ms
+  // with most commands taking only a few us
+  uint32_t start = millis();
+  while (this->busy_pin_->digital_read()) {
+    if (millis() - start > BUSY_TIMEOUT_MS) {
+      ESP_LOGE(TAG, "Wait busy timeout");
+      this->mark_failed();
+      break;
+    }
+  }
+}
+
+void SX126x::dump_config() {
+  ESP_LOGCONFIG(TAG, "SX126x:");
+  LOG_PIN("  CS Pin: ", this->cs_);
+  LOG_PIN("  BUSY Pin: ", this->busy_pin_);
+  LOG_PIN("  RST Pin: ", this->rst_pin_);
+  LOG_PIN("  DIO1 Pin: ", this->dio1_pin_);
+  ESP_LOGCONFIG(TAG,
+                "  HW Version: %15s\n"
+                "  Frequency: %" PRIu32 " Hz\n"
+                "  Bandwidth: %" PRIu32 " Hz\n"
+                "  PA Power: %" PRId8 " dBm\n"
+                "  PA Ramp: %" PRIu16 " us\n"
+                "  Payload Length: %" PRIu32 "\n"
+                "  CRC Enable: %s\n"
+                "  Rx Start: %s",
+                this->version_, this->frequency_, BW_HZ[this->bandwidth_], this->pa_power_, RAMP[this->pa_ramp_],
+                this->payload_length_, TRUEFALSE(this->crc_enable_), TRUEFALSE(this->rx_start_));
+  if (this->modulation_ == PACKET_TYPE_GFSK) {
+    const char *shaping = "NONE";
+    if (this->shaping_ == GAUSSIAN_BT_0_3) {
+      shaping = "GAUSSIAN_BT_0_3";
+    } else if (this->shaping_ == GAUSSIAN_BT_0_5) {
+      shaping = "GAUSSIAN_BT_0_5";
+    } else if (this->shaping_ == GAUSSIAN_BT_0_7) {
+      shaping = "GAUSSIAN_BT_0_7";
+    } else if (this->shaping_ == GAUSSIAN_BT_1_0) {
+      shaping = "GAUSSIAN_BT_1_0";
+    }
+    ESP_LOGCONFIG(TAG,
+                  "  Modulation: FSK\n"
+                  "  Deviation: %" PRIu32 " Hz\n"
+                  "  Shaping: %s\n"
+                  "  Preamble Size: %" PRIu16 "\n"
+                  "  Preamble Detect: %" PRIu16 "\n"
+                  "  Bitrate: %" PRIu32 "b/s",
+                  this->deviation_, shaping, this->preamble_size_, this->preamble_detect_, this->bitrate_);
+  } else if (this->modulation_ == PACKET_TYPE_LORA) {
+    const char *cr = "4/8";
+    if (this->coding_rate_ == LORA_CR_4_5) {
+      cr = "4/5";
+    } else if (this->coding_rate_ == LORA_CR_4_6) {
+      cr = "4/6";
+    } else if (this->coding_rate_ == LORA_CR_4_7) {
+      cr = "4/7";
+    }
+    ESP_LOGCONFIG(TAG,
+                  "  Modulation: LORA\n"
+                  "  Spreading Factor: %" PRIu8 "\n"
+                  "  Coding Rate: %s\n"
+                  "  Preamble Size: %" PRIu16,
+                  this->spreading_factor_, cr, this->preamble_size_);
+  }
+  if (!this->sync_value_.empty()) {
+    ESP_LOGCONFIG(TAG, "  Sync Value: 0x%s", format_hex(this->sync_value_).c_str());
+  }
+  if (this->is_failed()) {
+    ESP_LOGE(TAG, "Configuring SX126x failed");
+  }
+}
+
+}  // namespace sx126x
+}  // namespace esphome
diff --git a/esphome/components/sx126x/sx126x.h b/esphome/components/sx126x/sx126x.h
new file mode 100644
index 0000000000..fd5c37942d
--- /dev/null
+++ b/esphome/components/sx126x/sx126x.h
@@ -0,0 +1,140 @@
+#pragma once
+
+#include "esphome/components/spi/spi.h"
+#include "esphome/core/automation.h"
+#include "esphome/core/component.h"
+#include "sx126x_reg.h"
+#include 
+#include 
+
+namespace esphome {
+namespace sx126x {
+
+enum SX126xBw : uint8_t {
+  // FSK
+  SX126X_BW_4800,
+  SX126X_BW_5800,
+  SX126X_BW_7300,
+  SX126X_BW_9700,
+  SX126X_BW_11700,
+  SX126X_BW_14600,
+  SX126X_BW_19500,
+  SX126X_BW_23400,
+  SX126X_BW_29300,
+  SX126X_BW_39000,
+  SX126X_BW_46900,
+  SX126X_BW_58600,
+  SX126X_BW_78200,
+  SX126X_BW_93800,
+  SX126X_BW_117300,
+  SX126X_BW_156200,
+  SX126X_BW_187200,
+  SX126X_BW_234300,
+  SX126X_BW_312000,
+  SX126X_BW_373600,
+  SX126X_BW_467000,
+  // LORA
+  SX126X_BW_7810,
+  SX126X_BW_10420,
+  SX126X_BW_15630,
+  SX126X_BW_20830,
+  SX126X_BW_31250,
+  SX126X_BW_41670,
+  SX126X_BW_62500,
+  SX126X_BW_125000,
+  SX126X_BW_250000,
+  SX126X_BW_500000,
+};
+
+enum class SX126xError { NONE = 0, TIMEOUT, INVALID_PARAMS };
+
+class SX126xListener {
+ public:
+  virtual void on_packet(const std::vector &packet, float rssi, float snr) = 0;
+};
+
+class SX126x : public Component,
+               public spi::SPIDevice {
+ public:
+  size_t get_max_packet_size();
+  float get_setup_priority() const override { return setup_priority::PROCESSOR; }
+  void setup() override;
+  void loop() override;
+  void dump_config() override;
+  void set_bandwidth(SX126xBw bandwidth) { this->bandwidth_ = bandwidth; }
+  void set_bitrate(uint32_t bitrate) { this->bitrate_ = bitrate; }
+  void set_busy_pin(InternalGPIOPin *busy_pin) { this->busy_pin_ = busy_pin; }
+  void set_coding_rate(uint8_t coding_rate) { this->coding_rate_ = coding_rate; }
+  void set_crc_enable(bool crc_enable) { this->crc_enable_ = crc_enable; }
+  void set_deviation(uint32_t deviation) { this->deviation_ = deviation; }
+  void set_dio1_pin(InternalGPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; }
+  void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
+  void set_hw_version(const std::string &hw_version) { this->hw_version_ = hw_version; }
+  void set_mode_rx();
+  void set_mode_tx();
+  void set_mode_standby(SX126xStandbyMode mode);
+  void set_mode_sleep();
+  void set_modulation(uint8_t modulation) { this->modulation_ = modulation; }
+  void set_pa_power(int8_t power) { this->pa_power_ = power; }
+  void set_pa_ramp(uint8_t ramp) { this->pa_ramp_ = ramp; }
+  void set_payload_length(uint8_t payload_length) { this->payload_length_ = payload_length; }
+  void set_preamble_detect(uint16_t preamble_detect) { this->preamble_detect_ = preamble_detect; }
+  void set_preamble_size(uint16_t preamble_size) { this->preamble_size_ = preamble_size; }
+  void set_rst_pin(InternalGPIOPin *rst_pin) { this->rst_pin_ = rst_pin; }
+  void set_rx_start(bool rx_start) { this->rx_start_ = rx_start; }
+  void set_rf_switch(bool rf_switch) { this->rf_switch_ = rf_switch; }
+  void set_shaping(uint8_t shaping) { this->shaping_ = shaping; }
+  void set_spreading_factor(uint8_t spreading_factor) { this->spreading_factor_ = spreading_factor; }
+  void set_sync_value(const std::vector &sync_value) { this->sync_value_ = sync_value; }
+  void set_tcxo_voltage(uint8_t tcxo_voltage) { this->tcxo_voltage_ = tcxo_voltage; }
+  void set_tcxo_delay(uint32_t tcxo_delay) { this->tcxo_delay_ = tcxo_delay; }
+  void run_image_cal();
+  void configure();
+  SX126xError transmit_packet(const std::vector &packet);
+  void register_listener(SX126xListener *listener) { this->listeners_.push_back(listener); }
+  Trigger, float, float> *get_packet_trigger() const { return this->packet_trigger_; };
+
+ protected:
+  void configure_fsk_ook_();
+  void configure_lora_();
+  void set_packet_params_(uint8_t payload_length);
+  uint8_t read_fifo_(uint8_t offset, std::vector &packet);
+  void write_fifo_(uint8_t offset, const std::vector &packet);
+  void write_opcode_(uint8_t opcode, uint8_t *data, uint8_t size);
+  uint8_t read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size);
+  void write_register_(uint16_t reg, uint8_t *data, uint8_t size);
+  void read_register_(uint16_t reg, uint8_t *data, uint8_t size);
+  void call_listeners_(const std::vector &packet, float rssi, float snr);
+  void wait_busy_();
+  Trigger, float, float> *packet_trigger_{new Trigger, float, float>()};
+  std::vector listeners_;
+  std::vector packet_;
+  std::vector sync_value_;
+  InternalGPIOPin *busy_pin_{nullptr};
+  InternalGPIOPin *dio1_pin_{nullptr};
+  InternalGPIOPin *rst_pin_{nullptr};
+  std::string hw_version_;
+  char version_[16];
+  SX126xBw bandwidth_{SX126X_BW_125000};
+  uint32_t bitrate_{0};
+  uint32_t deviation_{0};
+  uint32_t frequency_{0};
+  uint32_t payload_length_{0};
+  uint32_t tcxo_delay_{0};
+  uint16_t preamble_detect_{0};
+  uint16_t preamble_size_{0};
+  uint8_t tcxo_voltage_{0};
+  uint8_t coding_rate_{0};
+  uint8_t modulation_{PACKET_TYPE_LORA};
+  uint8_t pa_ramp_{0};
+  uint8_t shaping_{0};
+  uint8_t spreading_factor_{0};
+  int8_t pa_power_{0};
+  bool crc_enable_{false};
+  bool rx_start_{false};
+  bool rf_switch_{false};
+};
+
+}  // namespace sx126x
+}  // namespace esphome
diff --git a/esphome/components/sx126x/sx126x_reg.h b/esphome/components/sx126x/sx126x_reg.h
new file mode 100644
index 0000000000..3b12d822b5
--- /dev/null
+++ b/esphome/components/sx126x/sx126x_reg.h
@@ -0,0 +1,163 @@
+#pragma once
+
+#include "esphome/core/hal.h"
+
+namespace esphome {
+namespace sx126x {
+
+static const uint32_t XTAL_FREQ = 32000000;
+
+enum SX126xOpCode : uint8_t {
+  RADIO_GET_STATUS = 0xC0,
+  RADIO_WRITE_REGISTER = 0x0D,
+  RADIO_READ_REGISTER = 0x1D,
+  RADIO_WRITE_BUFFER = 0x0E,
+  RADIO_READ_BUFFER = 0x1E,
+  RADIO_SET_SLEEP = 0x84,
+  RADIO_SET_STANDBY = 0x80,
+  RADIO_SET_FS = 0xC1,
+  RADIO_SET_TX = 0x83,
+  RADIO_SET_RX = 0x82,
+  RADIO_SET_RXDUTYCYCLE = 0x94,
+  RADIO_SET_CAD = 0xC5,
+  RADIO_SET_TXCONTINUOUSWAVE = 0xD1,
+  RADIO_SET_TXCONTINUOUSPREAMBLE = 0xD2,
+  RADIO_SET_PACKETTYPE = 0x8A,
+  RADIO_GET_PACKETTYPE = 0x11,
+  RADIO_SET_RFFREQUENCY = 0x86,
+  RADIO_SET_TXPARAMS = 0x8E,
+  RADIO_SET_PACONFIG = 0x95,
+  RADIO_SET_CADPARAMS = 0x88,
+  RADIO_SET_BUFFERBASEADDRESS = 0x8F,
+  RADIO_SET_MODULATIONPARAMS = 0x8B,
+  RADIO_SET_PACKETPARAMS = 0x8C,
+  RADIO_GET_RXBUFFERSTATUS = 0x13,
+  RADIO_GET_PACKETSTATUS = 0x14,
+  RADIO_GET_RSSIINST = 0x15,
+  RADIO_GET_STATS = 0x10,
+  RADIO_RESET_STATS = 0x00,
+  RADIO_SET_DIOIRQPARAMS = 0x08,
+  RADIO_GET_IRQSTATUS = 0x12,
+  RADIO_CLR_IRQSTATUS = 0x02,
+  RADIO_CALIBRATE = 0x89,
+  RADIO_CALIBRATEIMAGE = 0x98,
+  RADIO_SET_REGULATORMODE = 0x96,
+  RADIO_GET_ERROR = 0x17,
+  RADIO_CLR_ERROR = 0x07,
+  RADIO_SET_TCXOMODE = 0x97,
+  RADIO_SET_TXFALLBACKMODE = 0x93,
+  RADIO_SET_RFSWITCHMODE = 0x9D,
+  RADIO_SET_STOPRXTIMERONPREAMBLE = 0x9F,
+  RADIO_SET_LORASYMBTIMEOUT = 0xA0,
+};
+
+enum SX126xRegister : uint16_t {
+  REG_VERSION_STRING = 0x0320,
+  REG_GFSK_SYNCWORD = 0x06C0,
+  REG_LORA_SYNCWORD = 0x0740,
+  REG_OCP = 0x08E7,
+};
+
+enum SX126xStandbyMode : uint8_t {
+  STDBY_RC = 0x00,
+  STDBY_XOSC = 0x01,
+};
+
+enum SX126xPacketType : uint8_t {
+  PACKET_TYPE_GFSK = 0x00,
+  PACKET_TYPE_LORA = 0x01,
+  PACKET_TYPE_LRHSS = 0x03,
+};
+
+enum SX126xFskBw : uint8_t {
+  FSK_BW_4800 = 0x1F,
+  FSK_BW_5800 = 0x17,
+  FSK_BW_7300 = 0x0F,
+  FSK_BW_9700 = 0x1E,
+  FSK_BW_11700 = 0x16,
+  FSK_BW_14600 = 0x0E,
+  FSK_BW_19500 = 0x1D,
+  FSK_BW_23400 = 0x15,
+  FSK_BW_29300 = 0x0D,
+  FSK_BW_39000 = 0x1C,
+  FSK_BW_46900 = 0x14,
+  FSK_BW_58600 = 0x0C,
+  FSK_BW_78200 = 0x1B,
+  FSK_BW_93800 = 0x13,
+  FSK_BW_117300 = 0x0B,
+  FSK_BW_156200 = 0x1A,
+  FSK_BW_187200 = 0x12,
+  FSK_BW_234300 = 0x0A,
+  FSK_BW_312000 = 0x19,
+  FSK_BW_373600 = 0x11,
+  FSK_BW_467000 = 0x09,
+};
+
+enum SX126xLoraBw : uint8_t {
+  LORA_BW_7810 = 0x00,
+  LORA_BW_10420 = 0x08,
+  LORA_BW_15630 = 0x01,
+  LORA_BW_20830 = 0x09,
+  LORA_BW_31250 = 0x02,
+  LORA_BW_41670 = 0x0A,
+  LORA_BW_62500 = 0x03,
+  LORA_BW_125000 = 0x04,
+  LORA_BW_250000 = 0x05,
+  LORA_BW_500000 = 0x06,
+};
+
+enum SX126xLoraCr : uint8_t {
+  LORA_CR_4_5 = 0x01,
+  LORA_CR_4_6 = 0x02,
+  LORA_CR_4_7 = 0x03,
+  LORA_CR_4_8 = 0x04,
+};
+
+enum SX126xIrqMasks : uint16_t {
+  IRQ_RADIO_NONE = 0x0000,
+  IRQ_TX_DONE = 0x0001,
+  IRQ_RX_DONE = 0x0002,
+  IRQ_PREAMBLE_DETECTED = 0x0004,
+  IRQ_SYNCWORD_VALID = 0x0008,
+  IRQ_HEADER_VALID = 0x0010,
+  IRQ_HEADER_ERROR = 0x0020,
+  IRQ_CRC_ERROR = 0x0040,
+  IRQ_CAD_DONE = 0x0080,
+  IRQ_CAD_ACTIVITY_DETECTED = 0x0100,
+  IRQ_RX_TX_TIMEOUT = 0x0200,
+  IRQ_RADIO_ALL = 0xFFFF,
+};
+
+enum SX126xTcxoCtrl : uint8_t {
+  TCXO_CTRL_1_6V = 0x00,
+  TCXO_CTRL_1_7V = 0x01,
+  TCXO_CTRL_1_8V = 0x02,
+  TCXO_CTRL_2_2V = 0x03,
+  TCXO_CTRL_2_4V = 0x04,
+  TCXO_CTRL_2_7V = 0x05,
+  TCXO_CTRL_3_0V = 0x06,
+  TCXO_CTRL_3_3V = 0x07,
+  TCXO_CTRL_NONE = 0xFF,
+};
+
+enum SX126xPulseShape : uint8_t {
+  NO_FILTER = 0x00,
+  GAUSSIAN_BT_0_3 = 0x08,
+  GAUSSIAN_BT_0_5 = 0x09,
+  GAUSSIAN_BT_0_7 = 0x0A,
+  GAUSSIAN_BT_1_0 = 0x0B,
+};
+
+enum SX126xRampTime : uint8_t {
+  PA_RAMP_10 = 0x00,
+  PA_RAMP_20 = 0x01,
+  PA_RAMP_40 = 0x02,
+  PA_RAMP_80 = 0x03,
+  PA_RAMP_200 = 0x04,
+  PA_RAMP_800 = 0x05,
+  PA_RAMP_1700 = 0x06,
+  PA_RAMP_3400 = 0x07,
+};
+
+}  // namespace sx126x
+}  // namespace esphome
diff --git a/tests/components/sx126x/common.yaml b/tests/components/sx126x/common.yaml
new file mode 100644
index 0000000000..3f888c3ce4
--- /dev/null
+++ b/tests/components/sx126x/common.yaml
@@ -0,0 +1,40 @@
+spi:
+  clk_pin: ${clk_pin}
+  mosi_pin: ${mosi_pin}
+  miso_pin: ${miso_pin}
+
+sx126x:
+  dio1_pin: ${dio1_pin}
+  cs_pin: ${cs_pin}
+  busy_pin: ${busy_pin}
+  rst_pin: ${rst_pin}
+  pa_power: 3
+  bandwidth: 125_0kHz
+  crc_enable: true
+  frequency: 433920000
+  modulation: LORA
+  rx_start: true
+  hw_version: sx1262
+  rf_switch: true
+  sync_value: [0x14, 0x24]
+  preamble_size: 8
+  spreading_factor: 7
+  coding_rate: CR_4_6
+  tcxo_voltage: 1_8V
+  tcxo_delay: 5ms
+  on_packet:
+    then:
+      - lambda: |-
+          ESP_LOGD("lambda", "packet %.2f %.2f %s", rssi, snr, format_hex(x).c_str());
+
+button:
+  - platform: template
+    name: "SX126x Button"
+    on_press:
+      then:
+        - sx126x.set_mode_standby
+        - sx126x.run_image_cal
+        - sx126x.set_mode_sleep
+        - sx126x.set_mode_rx
+        - sx126x.send_packet:
+            data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C]
diff --git a/tests/components/sx126x/test.esp32-ard.yaml b/tests/components/sx126x/test.esp32-ard.yaml
new file mode 100644
index 0000000000..9770f52229
--- /dev/null
+++ b/tests/components/sx126x/test.esp32-ard.yaml
@@ -0,0 +1,10 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO27
+  miso_pin: GPIO19
+  cs_pin: GPIO18
+  rst_pin: GPIO23
+  busy_pin: GPIO25
+  dio1_pin: GPIO26
+
+<<: !include common.yaml
diff --git a/tests/components/sx126x/test.esp32-c3-ard.yaml b/tests/components/sx126x/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..91450e24ce
--- /dev/null
+++ b/tests/components/sx126x/test.esp32-c3-ard.yaml
@@ -0,0 +1,10 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO18
+  miso_pin: GPIO19
+  cs_pin: GPIO1
+  rst_pin: GPIO2
+  busy_pin: GPIO4
+  dio1_pin: GPIO3
+
+<<: !include common.yaml
diff --git a/tests/components/sx126x/test.esp32-c3-idf.yaml b/tests/components/sx126x/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..91450e24ce
--- /dev/null
+++ b/tests/components/sx126x/test.esp32-c3-idf.yaml
@@ -0,0 +1,10 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO18
+  miso_pin: GPIO19
+  cs_pin: GPIO1
+  rst_pin: GPIO2
+  busy_pin: GPIO4
+  dio1_pin: GPIO3
+
+<<: !include common.yaml
diff --git a/tests/components/sx126x/test.esp32-idf.yaml b/tests/components/sx126x/test.esp32-idf.yaml
new file mode 100644
index 0000000000..9770f52229
--- /dev/null
+++ b/tests/components/sx126x/test.esp32-idf.yaml
@@ -0,0 +1,10 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO27
+  miso_pin: GPIO19
+  cs_pin: GPIO18
+  rst_pin: GPIO23
+  busy_pin: GPIO25
+  dio1_pin: GPIO26
+
+<<: !include common.yaml
diff --git a/tests/components/sx126x/test.esp8266-ard.yaml b/tests/components/sx126x/test.esp8266-ard.yaml
new file mode 100644
index 0000000000..d2c07c5bb7
--- /dev/null
+++ b/tests/components/sx126x/test.esp8266-ard.yaml
@@ -0,0 +1,10 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO13
+  miso_pin: GPIO12
+  cs_pin: GPIO1
+  rst_pin: GPIO2
+  busy_pin: GPIO4
+  dio1_pin: GPIO3
+
+<<: !include common.yaml
diff --git a/tests/components/sx126x/test.rp2040-ard.yaml b/tests/components/sx126x/test.rp2040-ard.yaml
new file mode 100644
index 0000000000..8881e96971
--- /dev/null
+++ b/tests/components/sx126x/test.rp2040-ard.yaml
@@ -0,0 +1,10 @@
+substitutions:
+  clk_pin: GPIO2
+  mosi_pin: GPIO3
+  miso_pin: GPIO4
+  cs_pin: GPIO5
+  rst_pin: GPIO6
+  busy_pin: GPIO8
+  dio1_pin: GPIO7
+
+<<: !include common.yaml

From 4e25b6da7bcb67729aca90460fc6c83c856601e1 Mon Sep 17 00:00:00 2001
From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com>
Date: Mon, 7 Jul 2025 11:15:13 +0200
Subject: [PATCH 254/293] [nextion] Optimize settings memory usage with
 compile-time defines (#9350)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/nextion/display.py  |  6 ++-
 esphome/components/nextion/nextion.cpp | 53 ++++++++++++--------------
 esphome/components/nextion/nextion.h   | 34 +----------------
 3 files changed, 31 insertions(+), 62 deletions(-)

diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py
index 420f8f69c5..3ec80a6bcc 100644
--- a/esphome/components/nextion/display.py
+++ b/esphome/components/nextion/display.py
@@ -172,9 +172,11 @@ async def to_code(config):
 
     cg.add(var.set_auto_wake_on_touch(config[CONF_AUTO_WAKE_ON_TOUCH]))
 
-    cg.add(var.set_exit_reparse_on_start(config[CONF_EXIT_REPARSE_ON_START]))
+    if config[CONF_EXIT_REPARSE_ON_START]:
+        cg.add_define("USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START")
 
-    cg.add(var.set_skip_connection_handshake(config[CONF_SKIP_CONNECTION_HANDSHAKE]))
+    if config[CONF_SKIP_CONNECTION_HANDSHAKE]:
+        cg.add_define("USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE")
 
     if max_commands_per_loop := config.get(CONF_MAX_COMMANDS_PER_LOOP):
         cg.add_define("USE_NEXTION_MAX_COMMANDS_PER_LOOP")
diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp
index d95238bbb4..ed23ffe2c3 100644
--- a/esphome/components/nextion/nextion.cpp
+++ b/esphome/components/nextion/nextion.cpp
@@ -51,24 +51,19 @@ bool Nextion::check_connect_() {
   if (this->connection_state_.is_connected_)
     return true;
 
-  // Check if the handshake should be skipped for the Nextion connection
-  if (this->skip_connection_handshake_) {
-    // Log the connection status without handshake
-    ESP_LOGW(TAG, "Connected (no handshake)");
-    // Set the connection status to true
-    this->connection_state_.is_connected_ = true;
-    // Return true indicating the connection is set
-    return true;
-  }
-
+#ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
+  ESP_LOGW(TAG, "Connected (no handshake)");  // Log the connection status without handshake
+  this->is_connected_ = true;                 // Set the connection status to true
+  return true;                                // Return true indicating the connection is set
+#else                                         // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
   if (this->comok_sent_ == 0) {
     this->reset_(false);
 
     this->connection_state_.ignore_is_setup_ = true;
     this->send_command_("boguscommand=0");  // bogus command. needed sometimes after updating
-    if (this->exit_reparse_on_start_) {
-      this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN");
-    }
+#ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
+    this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN");
+#endif  // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
     this->send_command_("connect");
 
     this->comok_sent_ = App.get_loop_component_start_time();
@@ -94,7 +89,7 @@ bool Nextion::check_connect_() {
     for (size_t i = 0; i < response.length(); i++) {
       ESP_LOGN(TAG, "resp: %s %d %d %c", response.c_str(), i, response[i], response[i]);
     }
-#endif
+#endif  // NEXTION_PROTOCOL_LOG
 
     ESP_LOGW(TAG, "Not connected");
     comok_sent_ = 0;
@@ -130,6 +125,7 @@ bool Nextion::check_connect_() {
   this->connection_state_.ignore_is_setup_ = false;
   this->dump_config();
   return true;
+#endif  // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
 }
 
 void Nextion::reset_(bool reset_nextion) {
@@ -144,21 +140,22 @@ void Nextion::reset_(bool reset_nextion) {
 
 void Nextion::dump_config() {
   ESP_LOGCONFIG(TAG, "Nextion:");
-  if (this->skip_connection_handshake_) {
-    ESP_LOGCONFIG(TAG, "  Skip handshake: %s", YESNO(this->skip_connection_handshake_));
-  } else {
-    ESP_LOGCONFIG(TAG,
-                  "  Device Model:   %s\n"
-                  "  FW Version:     %s\n"
-                  "  Serial Number:  %s\n"
-                  "  Flash Size:     %s",
-                  this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(),
-                  this->flash_size_.c_str());
-  }
+#ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
+  ESP_LOGCONFIG(TAG, "  Skip handshake: YES");
+#else  // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
   ESP_LOGCONFIG(TAG,
-                "  Wake On Touch:  %s\n"
-                "  Exit reparse:   %s",
-                YESNO(this->connection_state_.auto_wake_on_touch_), YESNO(this->exit_reparse_on_start_));
+                "  Device Model:   %s\n"
+                "  FW Version:     %s\n"
+                "  Serial Number:  %s\n"
+                "  Flash Size:     %s\n"
+#ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
+                "  Exit reparse:   YES\n"
+#endif  // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
+                "  Wake On Touch:  %s",
+                this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(),
+                this->flash_size_.c_str(), YESNO(this->auto_wake_on_touch_));
+#endif  // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
+
 #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
   ESP_LOGCONFIG(TAG, "  Max commands per loop: %u", this->max_commands_per_loop_);
 #endif  // USE_NEXTION_MAX_COMMANDS_PER_LOOP
diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h
index 0ce9429594..efa607ef8e 100644
--- a/esphome/components/nextion/nextion.h
+++ b/esphome/components/nextion/nextion.h
@@ -932,21 +932,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
    */
   void set_backlight_brightness(float brightness);
 
-  /**
-   * Sets whether the Nextion display should skip the connection handshake process.
-   * @param skip_handshake True or false. When skip_connection_handshake is true,
-   * the connection will be established without performing the handshake.
-   * This can be useful when using Nextion Simulator.
-   *
-   * Example:
-   * ```cpp
-   * it.set_skip_connection_handshake(true);
-   * ```
-   *
-   * When set to true, the display will be marked as connected without performing a handshake.
-   */
-  void set_skip_connection_handshake(bool skip_handshake) { this->skip_connection_handshake_ = skip_handshake; }
-
   /**
    * Sets Nextion mode between sleep and awake
    * @param True or false. Sleep=true to enter sleep mode or sleep=false to exit sleep mode.
@@ -1236,20 +1221,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
    */
   void set_auto_wake_on_touch(bool auto_wake_on_touch);
 
-  /**
-   * Sets if Nextion should exit the active reparse mode before the "connect" command is sent
-   * @param exit_reparse_on_start True or false. When exit_reparse_on_start is true, the exit reparse command
-   * will be sent before requesting the connection from Nextion.
-   *
-   * Example:
-   * ```cpp
-   * it.set_exit_reparse_on_start(true);
-   * ```
-   *
-   * The display will be requested to leave active reparse mode before setup.
-   */
-  void set_exit_reparse_on_start(bool exit_reparse_on_start) { this->exit_reparse_on_start_ = exit_reparse_on_start; }
-
   /**
    * @brief Retrieves the number of commands pending in the Nextion command queue.
    *
@@ -1292,7 +1263,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
    * the Nextion display. A connection is considered established when:
    *
    * - The initial handshake with the display is completed successfully, or
-   * - The handshake is skipped via skip_connection_handshake_ flag
+   * - The handshake is skipped via USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE flag
    *
    * The connection status is particularly useful when:
    * - Troubleshooting communication issues
@@ -1358,8 +1329,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
 #ifdef USE_NEXTION_CONF_START_UP_PAGE
   uint8_t start_up_page_ = 255;
 #endif  // USE_NEXTION_CONF_START_UP_PAGE
-  bool exit_reparse_on_start_ = false;
-  bool skip_connection_handshake_ = false;
+  bool auto_wake_on_touch_ = true;
 
   /**
    * Manually send a raw command to the display and don't wait for an acknowledgement packet.

From db877e688ac62d4f8af8bac885791d9f99f0f950 Mon Sep 17 00:00:00 2001
From: Keith Burzinski 
Date: Mon, 7 Jul 2025 07:22:49 -0500
Subject: [PATCH 255/293] [ld2450] Clean-up for consistency, reduce CPU usage
 when idle (#9363)

---
 esphome/components/ld2450/button/__init__.py  |   6 +-
 .../ld2450/button/factory_reset_button.cpp    |   9 +
 ...{reset_button.h => factory_reset_button.h} |   4 +-
 .../components/ld2450/button/reset_button.cpp |   9 -
 esphome/components/ld2450/ld2450.cpp          | 434 ++++++++++--------
 esphome/components/ld2450/ld2450.h            |  49 +-
 6 files changed, 280 insertions(+), 231 deletions(-)
 create mode 100644 esphome/components/ld2450/button/factory_reset_button.cpp
 rename esphome/components/ld2450/button/{reset_button.h => factory_reset_button.h} (65%)
 delete mode 100644 esphome/components/ld2450/button/reset_button.cpp

diff --git a/esphome/components/ld2450/button/__init__.py b/esphome/components/ld2450/button/__init__.py
index 39671d3a3b..429aa59389 100644
--- a/esphome/components/ld2450/button/__init__.py
+++ b/esphome/components/ld2450/button/__init__.py
@@ -13,13 +13,13 @@ from esphome.const import (
 
 from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns
 
-ResetButton = ld2450_ns.class_("ResetButton", button.Button)
+FactoryResetButton = ld2450_ns.class_("FactoryResetButton", button.Button)
 RestartButton = ld2450_ns.class_("RestartButton", button.Button)
 
 CONFIG_SCHEMA = {
     cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
     cv.Optional(CONF_FACTORY_RESET): button.button_schema(
-        ResetButton,
+        FactoryResetButton,
         device_class=DEVICE_CLASS_RESTART,
         entity_category=ENTITY_CATEGORY_CONFIG,
         icon=ICON_RESTART_ALERT,
@@ -38,7 +38,7 @@ async def to_code(config):
     if factory_reset_config := config.get(CONF_FACTORY_RESET):
         b = await button.new_button(factory_reset_config)
         await cg.register_parented(b, config[CONF_LD2450_ID])
-        cg.add(ld2450_component.set_reset_button(b))
+        cg.add(ld2450_component.set_factory_reset_button(b))
     if restart_config := config.get(CONF_RESTART):
         b = await button.new_button(restart_config)
         await cg.register_parented(b, config[CONF_LD2450_ID])
diff --git a/esphome/components/ld2450/button/factory_reset_button.cpp b/esphome/components/ld2450/button/factory_reset_button.cpp
new file mode 100644
index 0000000000..bcac7ada2f
--- /dev/null
+++ b/esphome/components/ld2450/button/factory_reset_button.cpp
@@ -0,0 +1,9 @@
+#include "factory_reset_button.h"
+
+namespace esphome {
+namespace ld2450 {
+
+void FactoryResetButton::press_action() { this->parent_->factory_reset(); }
+
+}  // namespace ld2450
+}  // namespace esphome
diff --git a/esphome/components/ld2450/button/reset_button.h b/esphome/components/ld2450/button/factory_reset_button.h
similarity index 65%
rename from esphome/components/ld2450/button/reset_button.h
rename to esphome/components/ld2450/button/factory_reset_button.h
index 73804fa6d6..8e80347119 100644
--- a/esphome/components/ld2450/button/reset_button.h
+++ b/esphome/components/ld2450/button/factory_reset_button.h
@@ -6,9 +6,9 @@
 namespace esphome {
 namespace ld2450 {
 
-class ResetButton : public button::Button, public Parented {
+class FactoryResetButton : public button::Button, public Parented {
  public:
-  ResetButton() = default;
+  FactoryResetButton() = default;
 
  protected:
   void press_action() override;
diff --git a/esphome/components/ld2450/button/reset_button.cpp b/esphome/components/ld2450/button/reset_button.cpp
deleted file mode 100644
index e96ec99cc5..0000000000
--- a/esphome/components/ld2450/button/reset_button.cpp
+++ /dev/null
@@ -1,9 +0,0 @@
-#include "reset_button.h"
-
-namespace esphome {
-namespace ld2450 {
-
-void ResetButton::press_action() { this->parent_->factory_reset(); }
-
-}  // namespace ld2450
-}  // namespace esphome
diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp
index 4b87f1cea4..8f3b3a3f21 100644
--- a/esphome/components/ld2450/ld2450.cpp
+++ b/esphome/components/ld2450/ld2450.cpp
@@ -18,11 +18,10 @@ namespace esphome {
 namespace ld2450 {
 
 static const char *const TAG = "ld2450";
-static const char *const NO_MAC = "08:05:04:03:02:01";
 static const char *const UNKNOWN_MAC = "unknown";
 static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
 
-enum BaudRateStructure : uint8_t {
+enum BaudRate : uint8_t {
   BAUD_RATE_9600 = 1,
   BAUD_RATE_19200 = 2,
   BAUD_RATE_38400 = 3,
@@ -33,14 +32,13 @@ enum BaudRateStructure : uint8_t {
   BAUD_RATE_460800 = 8
 };
 
-// Zone type struct
-enum ZoneTypeStructure : uint8_t {
+enum ZoneType : uint8_t {
   ZONE_DISABLED = 0,
   ZONE_DETECTION = 1,
   ZONE_FILTER = 2,
 };
 
-enum PeriodicDataStructure : uint8_t {
+enum PeriodicData : uint8_t {
   TARGET_X = 4,
   TARGET_Y = 6,
   TARGET_SPEED = 8,
@@ -48,12 +46,12 @@ enum PeriodicDataStructure : uint8_t {
 };
 
 enum PeriodicDataValue : uint8_t {
-  HEAD = 0xAA,
-  END = 0x55,
+  HEADER = 0xAA,
+  FOOTER = 0x55,
   CHECK = 0x00,
 };
 
-enum AckDataStructure : uint8_t {
+enum AckData : uint8_t {
   COMMAND = 6,
   COMMAND_STATUS = 7,
 };
@@ -61,11 +59,11 @@ enum AckDataStructure : uint8_t {
 // Memory-efficient lookup tables
 struct StringToUint8 {
   const char *str;
-  uint8_t value;
+  const uint8_t value;
 };
 
 struct Uint8ToString {
-  uint8_t value;
+  const uint8_t value;
   const char *str;
 };
 
@@ -75,6 +73,13 @@ constexpr StringToUint8 BAUD_RATES_BY_STR[] = {
     {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800},
 };
 
+constexpr Uint8ToString DIRECTION_BY_UINT[] = {
+    {DIRECTION_APPROACHING, "Approaching"},
+    {DIRECTION_MOVING_AWAY, "Moving away"},
+    {DIRECTION_STATIONARY, "Stationary"},
+    {DIRECTION_NA, "NA"},
+};
+
 constexpr Uint8ToString ZONE_TYPE_BY_UINT[] = {
     {ZONE_DISABLED, "Disabled"},
     {ZONE_DETECTION, "Detection"},
@@ -104,28 +109,35 @@ template const char *find_str(const Uint8ToString (&arr)[N], uint8_t v
   return "";  // Not found
 }
 
-// LD2450 serial command header & footer
-static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA};
-static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01};
 // LD2450 UART Serial Commands
-static const uint8_t CMD_ENABLE_CONF = 0xFF;
-static const uint8_t CMD_DISABLE_CONF = 0xFE;
-static const uint8_t CMD_VERSION = 0xA0;
-static const uint8_t CMD_MAC = 0xA5;
-static const uint8_t CMD_RESET = 0xA2;
-static const uint8_t CMD_RESTART = 0xA3;
-static const uint8_t CMD_BLUETOOTH = 0xA4;
-static const uint8_t CMD_SINGLE_TARGET_MODE = 0x80;
-static const uint8_t CMD_MULTI_TARGET_MODE = 0x90;
-static const uint8_t CMD_QUERY_TARGET_MODE = 0x91;
-static const uint8_t CMD_SET_BAUD_RATE = 0xA1;
-static const uint8_t CMD_QUERY_ZONE = 0xC1;
-static const uint8_t CMD_SET_ZONE = 0xC2;
+static constexpr uint8_t CMD_ENABLE_CONF = 0xFF;
+static constexpr uint8_t CMD_DISABLE_CONF = 0xFE;
+static constexpr uint8_t CMD_QUERY_VERSION = 0xA0;
+static constexpr uint8_t CMD_QUERY_MAC_ADDRESS = 0xA5;
+static constexpr uint8_t CMD_RESET = 0xA2;
+static constexpr uint8_t CMD_RESTART = 0xA3;
+static constexpr uint8_t CMD_BLUETOOTH = 0xA4;
+static constexpr uint8_t CMD_SINGLE_TARGET_MODE = 0x80;
+static constexpr uint8_t CMD_MULTI_TARGET_MODE = 0x90;
+static constexpr uint8_t CMD_QUERY_TARGET_MODE = 0x91;
+static constexpr uint8_t CMD_SET_BAUD_RATE = 0xA1;
+static constexpr uint8_t CMD_QUERY_ZONE = 0xC1;
+static constexpr uint8_t CMD_SET_ZONE = 0xC2;
+// Header & Footer size
+static constexpr uint8_t HEADER_FOOTER_SIZE = 4;
+// Command Header & Footer
+static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA};
+static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01};
+// Data Header & Footer
+static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xAA, 0xFF, 0x03, 0x00};
+static constexpr uint8_t DATA_FRAME_FOOTER[2] = {0x55, 0xCC};
+// MAC address the module uses when Bluetooth is disabled
+static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
 
 static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; };
 
 static inline void convert_int_values_to_hex(const int *values, uint8_t *bytes) {
-  for (int i = 0; i < 4; i++) {
+  for (uint8_t i = 0; i < 4; i++) {
     uint16_t val = values[i] & 0xFFFF;
     bytes[i * 2] = val & 0xFF;             // Store low byte first (little-endian)
     bytes[i * 2 + 1] = (val >> 8) & 0xFF;  // Store high byte second
@@ -166,18 +178,13 @@ static inline float calculate_angle(float base, float hypotenuse) {
   return angle_degrees;
 }
 
-static inline std::string get_direction(int16_t speed) {
-  static const char *const APPROACHING = "Approaching";
-  static const char *const MOVING_AWAY = "Moving away";
-  static const char *const STATIONARY = "Stationary";
-
-  if (speed > 0) {
-    return MOVING_AWAY;
+static bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
+  for (uint8_t i = 0; i < HEADER_FOOTER_SIZE; i++) {
+    if (header_footer[i] != buffer[i]) {
+      return false;  // Mismatch in header/footer
+    }
   }
-  if (speed < 0) {
-    return APPROACHING;
-  }
-  return STATIONARY;
+  return true;  // Valid header/footer
 }
 
 void LD2450Component::setup() {
@@ -192,84 +199,93 @@ void LD2450Component::setup() {
 }
 
 void LD2450Component::dump_config() {
-  ESP_LOGCONFIG(TAG, "LD2450:");
+  std::string mac_str =
+      mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
+  std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
+                                    this->version_[4], this->version_[3], this->version_[2]);
+  ESP_LOGCONFIG(TAG,
+                "LD2450:\n"
+                "  Firmware version: %s\n"
+                "  MAC address: %s\n"
+                "  Throttle: %u ms",
+                version.c_str(), mac_str.c_str(), this->throttle_);
 #ifdef USE_BINARY_SENSOR
-  LOG_BINARY_SENSOR("  ", "TargetBinarySensor", this->target_binary_sensor_);
-  LOG_BINARY_SENSOR("  ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_);
-  LOG_BINARY_SENSOR("  ", "StillTargetBinarySensor", this->still_target_binary_sensor_);
-#endif
-#ifdef USE_SWITCH
-  LOG_SWITCH("  ", "BluetoothSwitch", this->bluetooth_switch_);
-  LOG_SWITCH("  ", "MultiTargetSwitch", this->multi_target_switch_);
-#endif
-#ifdef USE_BUTTON
-  LOG_BUTTON("  ", "ResetButton", this->reset_button_);
-  LOG_BUTTON("  ", "RestartButton", this->restart_button_);
+  ESP_LOGCONFIG(TAG, "Binary Sensors:");
+  LOG_BINARY_SENSOR("  ", "MovingTarget", this->moving_target_binary_sensor_);
+  LOG_BINARY_SENSOR("  ", "StillTarget", this->still_target_binary_sensor_);
+  LOG_BINARY_SENSOR("  ", "Target", this->target_binary_sensor_);
 #endif
 #ifdef USE_SENSOR
-  LOG_SENSOR("  ", "TargetCountSensor", this->target_count_sensor_);
-  LOG_SENSOR("  ", "StillTargetCountSensor", this->still_target_count_sensor_);
-  LOG_SENSOR("  ", "MovingTargetCountSensor", this->moving_target_count_sensor_);
+  ESP_LOGCONFIG(TAG, "Sensors:");
+  LOG_SENSOR("  ", "MovingTargetCount", this->moving_target_count_sensor_);
+  LOG_SENSOR("  ", "StillTargetCount", this->still_target_count_sensor_);
+  LOG_SENSOR("  ", "TargetCount", this->target_count_sensor_);
   for (sensor::Sensor *s : this->move_x_sensors_) {
-    LOG_SENSOR("  ", "NthTargetXSensor", s);
+    LOG_SENSOR("  ", "TargetX", s);
   }
   for (sensor::Sensor *s : this->move_y_sensors_) {
-    LOG_SENSOR("  ", "NthTargetYSensor", s);
-  }
-  for (sensor::Sensor *s : this->move_speed_sensors_) {
-    LOG_SENSOR("  ", "NthTargetSpeedSensor", s);
+    LOG_SENSOR("  ", "TargetY", s);
   }
   for (sensor::Sensor *s : this->move_angle_sensors_) {
-    LOG_SENSOR("  ", "NthTargetAngleSensor", s);
+    LOG_SENSOR("  ", "TargetAngle", s);
   }
   for (sensor::Sensor *s : this->move_distance_sensors_) {
-    LOG_SENSOR("  ", "NthTargetDistanceSensor", s);
+    LOG_SENSOR("  ", "TargetDistance", s);
   }
   for (sensor::Sensor *s : this->move_resolution_sensors_) {
-    LOG_SENSOR("  ", "NthTargetResolutionSensor", s);
+    LOG_SENSOR("  ", "TargetResolution", s);
+  }
+  for (sensor::Sensor *s : this->move_speed_sensors_) {
+    LOG_SENSOR("  ", "TargetSpeed", s);
   }
   for (sensor::Sensor *s : this->zone_target_count_sensors_) {
-    LOG_SENSOR("  ", "NthZoneTargetCountSensor", s);
-  }
-  for (sensor::Sensor *s : this->zone_still_target_count_sensors_) {
-    LOG_SENSOR("  ", "NthZoneStillTargetCountSensor", s);
+    LOG_SENSOR("  ", "ZoneTargetCount", s);
   }
   for (sensor::Sensor *s : this->zone_moving_target_count_sensors_) {
-    LOG_SENSOR("  ", "NthZoneMovingTargetCountSensor", s);
+    LOG_SENSOR("  ", "ZoneMovingTargetCount", s);
+  }
+  for (sensor::Sensor *s : this->zone_still_target_count_sensors_) {
+    LOG_SENSOR("  ", "ZoneStillTargetCount", s);
   }
 #endif
 #ifdef USE_TEXT_SENSOR
-  LOG_TEXT_SENSOR("  ", "VersionTextSensor", this->version_text_sensor_);
-  LOG_TEXT_SENSOR("  ", "MacTextSensor", this->mac_text_sensor_);
+  ESP_LOGCONFIG(TAG, "Text Sensors:");
+  LOG_TEXT_SENSOR("  ", "Version", this->version_text_sensor_);
+  LOG_TEXT_SENSOR("  ", "Mac", this->mac_text_sensor_);
   for (text_sensor::TextSensor *s : this->direction_text_sensors_) {
-    LOG_TEXT_SENSOR("  ", "NthDirectionTextSensor", s);
+    LOG_TEXT_SENSOR("  ", "Direction", s);
   }
 #endif
 #ifdef USE_NUMBER
+  ESP_LOGCONFIG(TAG, "Numbers:");
+  LOG_NUMBER("  ", "PresenceTimeout", this->presence_timeout_number_);
   for (auto n : this->zone_numbers_) {
-    LOG_NUMBER("  ", "ZoneX1Number", n.x1);
-    LOG_NUMBER("  ", "ZoneY1Number", n.y1);
-    LOG_NUMBER("  ", "ZoneX2Number", n.x2);
-    LOG_NUMBER("  ", "ZoneY2Number", n.y2);
+    LOG_NUMBER("  ", "ZoneX1", n.x1);
+    LOG_NUMBER("  ", "ZoneY1", n.y1);
+    LOG_NUMBER("  ", "ZoneX2", n.x2);
+    LOG_NUMBER("  ", "ZoneY2", n.y2);
   }
 #endif
 #ifdef USE_SELECT
-  LOG_SELECT("  ", "BaudRateSelect", this->baud_rate_select_);
-  LOG_SELECT("  ", "ZoneTypeSelect", this->zone_type_select_);
+  ESP_LOGCONFIG(TAG, "Selects:");
+  LOG_SELECT("  ", "BaudRate", this->baud_rate_select_);
+  LOG_SELECT("  ", "ZoneType", this->zone_type_select_);
 #endif
-#ifdef USE_NUMBER
-  LOG_NUMBER("  ", "PresenceTimeoutNumber", this->presence_timeout_number_);
+#ifdef USE_SWITCH
+  ESP_LOGCONFIG(TAG, "Switches:");
+  LOG_SWITCH("  ", "Bluetooth", this->bluetooth_switch_);
+  LOG_SWITCH("  ", "MultiTarget", this->multi_target_switch_);
+#endif
+#ifdef USE_BUTTON
+  ESP_LOGCONFIG(TAG, "Buttons:");
+  LOG_BUTTON("  ", "FactoryReset", this->factory_reset_button_);
+  LOG_BUTTON("  ", "Restart", this->restart_button_);
 #endif
-  ESP_LOGCONFIG(TAG,
-                "  Throttle: %ums\n"
-                "  MAC Address: %s\n"
-                "  Firmware version: %s",
-                this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str());
 }
 
 void LD2450Component::loop() {
   while (this->available()) {
-    this->readline_(read(), this->buffer_data_, MAX_LINE_LENGTH);
+    this->readline_(this->read());
   }
 }
 
@@ -304,7 +320,7 @@ void LD2450Component::set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_
   this->zone_type_ = zone_type;
   int zone_parameters[12] = {zone1_x1, zone1_y1, zone1_x2, zone1_y2, zone2_x1, zone2_y1,
                              zone2_x2, zone2_y2, zone3_x1, zone3_y1, zone3_x2, zone3_y2};
-  for (int i = 0; i < MAX_ZONES; i++) {
+  for (uint8_t i = 0; i < MAX_ZONES; i++) {
     this->zone_config_[i].x1 = zone_parameters[i * 4];
     this->zone_config_[i].y1 = zone_parameters[i * 4 + 1];
     this->zone_config_[i].x2 = zone_parameters[i * 4 + 2];
@@ -318,15 +334,15 @@ void LD2450Component::send_set_zone_command_() {
   uint8_t cmd_value[26] = {};
   uint8_t zone_type_bytes[2] = {static_cast(this->zone_type_), 0x00};
   uint8_t area_config[24] = {};
-  for (int i = 0; i < MAX_ZONES; i++) {
+  for (uint8_t i = 0; i < MAX_ZONES; i++) {
     int values[4] = {this->zone_config_[i].x1, this->zone_config_[i].y1, this->zone_config_[i].x2,
                      this->zone_config_[i].y2};
     ld2450::convert_int_values_to_hex(values, area_config + (i * 8));
   }
-  std::memcpy(cmd_value, zone_type_bytes, 2);
-  std::memcpy(cmd_value + 2, area_config, 24);
+  std::memcpy(cmd_value, zone_type_bytes, sizeof(zone_type_bytes));
+  std::memcpy(cmd_value + 2, area_config, sizeof(area_config));
   this->set_config_mode_(true);
-  this->send_command_(CMD_SET_ZONE, cmd_value, 26);
+  this->send_command_(CMD_SET_ZONE, cmd_value, sizeof(cmd_value));
   this->set_config_mode_(false);
 }
 
@@ -342,14 +358,14 @@ bool LD2450Component::get_timeout_status_(uint32_t check_millis) {
 }
 
 // Extract, store and publish zone details LD2450 buffer
-void LD2450Component::process_zone_(uint8_t *buffer) {
+void LD2450Component::process_zone_() {
   uint8_t index, start;
   for (index = 0; index < MAX_ZONES; index++) {
     start = 12 + index * 8;
-    this->zone_config_[index].x1 = ld2450::hex_to_signed_int(buffer, start);
-    this->zone_config_[index].y1 = ld2450::hex_to_signed_int(buffer, start + 2);
-    this->zone_config_[index].x2 = ld2450::hex_to_signed_int(buffer, start + 4);
-    this->zone_config_[index].y2 = ld2450::hex_to_signed_int(buffer, start + 6);
+    this->zone_config_[index].x1 = ld2450::hex_to_signed_int(this->buffer_data_, start);
+    this->zone_config_[index].y1 = ld2450::hex_to_signed_int(this->buffer_data_, start + 2);
+    this->zone_config_[index].x2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 4);
+    this->zone_config_[index].y2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 6);
 #ifdef USE_NUMBER
     // only one null check as all coordinates are required for a single zone
     if (this->zone_numbers_[index].x1 != nullptr) {
@@ -395,27 +411,25 @@ void LD2450Component::restart_and_read_all_info() {
 
 // Send command with values to LD2450
 void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
-  ESP_LOGV(TAG, "Sending command %02X", command);
-  // frame header
-  this->write_array(CMD_FRAME_HEADER, 4);
+  ESP_LOGV(TAG, "Sending COMMAND %02X", command);
+  // frame header bytes
+  this->write_array(CMD_FRAME_HEADER, sizeof(CMD_FRAME_HEADER));
   // length bytes
-  int len = 2;
+  uint8_t len = 2;
   if (command_value != nullptr) {
     len += command_value_len;
   }
-  this->write_byte(lowbyte(len));
-  this->write_byte(highbyte(len));
-  // command
-  this->write_byte(lowbyte(command));
-  this->write_byte(highbyte(command));
+  uint8_t len_cmd[] = {lowbyte(len), highbyte(len), command, 0x00};
+  this->write_array(len_cmd, sizeof(len_cmd));
+
   // command value bytes
   if (command_value != nullptr) {
-    for (int i = 0; i < command_value_len; i++) {
+    for (uint8_t i = 0; i < command_value_len; i++) {
       this->write_byte(command_value[i]);
     }
   }
-  // footer
-  this->write_array(CMD_FRAME_END, 4);
+  // frame footer bytes
+  this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER));
   // FIXME to remove
   delay(50);  // NOLINT
 }
@@ -423,26 +437,23 @@ void LD2450Component::send_command_(uint8_t command, const uint8_t *command_valu
 // LD2450 Radar data message:
 //  [AA FF 03 00] [0E 03 B1 86 10 00 40 01] [00 00 00 00 00 00 00 00] [00 00 00 00 00 00 00 00] [55 CC]
 //   Header       Target 1                  Target 2                  Target 3                  End
-void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
+void LD2450Component::handle_periodic_data_() {
   // Early throttle check - moved before any processing to save CPU cycles
   if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) {
-    ESP_LOGV(TAG, "Throttling: %d", this->throttle_);
     return;
   }
 
-  if (len < 29) {  // header (4 bytes) + 8 x 3 target data + footer (2 bytes)
-    ESP_LOGE(TAG, "Invalid message length");
+  if (this->buffer_pos_ < 29) {  // header (4 bytes) + 8 x 3 target data + footer (2 bytes)
+    ESP_LOGE(TAG, "Invalid length");
     return;
   }
-  if (buffer[0] != 0xAA || buffer[1] != 0xFF || buffer[2] != 0x03 || buffer[3] != 0x00) {  // header
-    ESP_LOGE(TAG, "Invalid message header");
+  if (!ld2450::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) ||
+      this->buffer_data_[this->buffer_pos_ - 2] != DATA_FRAME_FOOTER[0] ||
+      this->buffer_data_[this->buffer_pos_ - 1] != DATA_FRAME_FOOTER[1]) {
+    ESP_LOGE(TAG, "Invalid header/footer");
     return;
   }
-  if (buffer[len - 2] != 0x55 || buffer[len - 1] != 0xCC) {  // footer
-    ESP_LOGE(TAG, "Invalid message footer");
-    return;
-  }
-
+  // Save the timestamp after validating the frame so, if invalid, we'll take the next frame immediately
   this->last_periodic_millis_ = App.get_loop_component_start_time();
 
   int16_t target_count = 0;
@@ -450,13 +461,13 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
   int16_t moving_target_count = 0;
   int16_t start = 0;
   int16_t val = 0;
-  uint8_t index = 0;
   int16_t tx = 0;
   int16_t ty = 0;
   int16_t td = 0;
   int16_t ts = 0;
   int16_t angle = 0;
-  std::string direction{};
+  uint8_t index = 0;
+  Direction direction{DIRECTION_UNDEFINED};
   bool is_moving = false;
 
 #if defined(USE_BINARY_SENSOR) || defined(USE_SENSOR) || defined(USE_TEXT_SENSOR)
@@ -468,7 +479,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
     is_moving = false;
     sensor::Sensor *sx = this->move_x_sensors_[index];
     if (sx != nullptr) {
-      val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]);
+      val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
       tx = val;
       if (this->cached_target_data_[index].x != val) {
         sx->publish_state(val);
@@ -479,7 +490,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
     start = TARGET_Y + index * 8;
     sensor::Sensor *sy = this->move_y_sensors_[index];
     if (sy != nullptr) {
-      val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]);
+      val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
       ty = val;
       if (this->cached_target_data_[index].y != val) {
         sy->publish_state(val);
@@ -490,7 +501,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
     start = TARGET_RESOLUTION + index * 8;
     sensor::Sensor *sr = this->move_resolution_sensors_[index];
     if (sr != nullptr) {
-      val = (buffer[start + 1] << 8) | buffer[start];
+      val = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start];
       if (this->cached_target_data_[index].resolution != val) {
         sr->publish_state(val);
         this->cached_target_data_[index].resolution = val;
@@ -499,7 +510,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
 #endif
     // SPEED
     start = TARGET_SPEED + index * 8;
-    val = ld2450::decode_speed(buffer[start], buffer[start + 1]);
+    val = ld2450::decode_speed(this->buffer_data_[start], this->buffer_data_[start + 1]);
     ts = val;
     if (val) {
       is_moving = true;
@@ -532,7 +543,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
       }
     }
     // ANGLE
-    angle = calculate_angle(static_cast(ty), static_cast(td));
+    angle = ld2450::calculate_angle(static_cast(ty), static_cast(td));
     if (tx > 0) {
       angle = angle * -1;
     }
@@ -547,14 +558,19 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
 #endif
 #ifdef USE_TEXT_SENSOR
     // DIRECTION
-    direction = get_direction(ts);
     if (td == 0) {
-      direction = "NA";
+      direction = DIRECTION_NA;
+    } else if (ts > 0) {
+      direction = DIRECTION_MOVING_AWAY;
+    } else if (ts < 0) {
+      direction = DIRECTION_APPROACHING;
+    } else {
+      direction = DIRECTION_STATIONARY;
     }
     text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
     if (tsd != nullptr) {
       if (this->cached_target_data_[index].direction != direction) {
-        tsd->publish_state(direction);
+        tsd->publish_state(find_str(ld2450::DIRECTION_BY_UINT, direction));
         this->cached_target_data_[index].direction = direction;
       }
     }
@@ -678,117 +694,139 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
 #endif
 }
 
-bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
-  ESP_LOGV(TAG, "Handling ack data for command %02X", buffer[COMMAND]);
-  if (len < 10) {
-    ESP_LOGE(TAG, "Invalid ack length");
+bool LD2450Component::handle_ack_data_() {
+  ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]);
+  if (this->buffer_pos_ < 10) {
+    ESP_LOGE(TAG, "Invalid length");
     return true;
   }
-  if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) {  // frame header
-    ESP_LOGE(TAG, "Invalid ack header (command %02X)", buffer[COMMAND]);
+  if (!ld2450::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) {
+    ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str());
     return true;
   }
-  if (buffer[COMMAND_STATUS] != 0x01) {
-    ESP_LOGE(TAG, "Invalid ack status");
+  if (this->buffer_data_[COMMAND_STATUS] != 0x01) {
+    ESP_LOGE(TAG, "Invalid status");
     return true;
   }
-  if (buffer[8] || buffer[9]) {
-    ESP_LOGE(TAG, "Last buffer was %u, %u", buffer[8], buffer[9]);
+  if (this->buffer_data_[8] || this->buffer_data_[9]) {
+    ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]);
     return true;
   }
 
-  switch (buffer[COMMAND]) {
-    case lowbyte(CMD_ENABLE_CONF):
-      ESP_LOGV(TAG, "Enable conf command");
+  switch (this->buffer_data_[COMMAND]) {
+    case CMD_ENABLE_CONF:
+      ESP_LOGV(TAG, "Enable conf");
       break;
-    case lowbyte(CMD_DISABLE_CONF):
-      ESP_LOGV(TAG, "Disable conf command");
+
+    case CMD_DISABLE_CONF:
+      ESP_LOGV(TAG, "Disabled conf");
       break;
-    case lowbyte(CMD_SET_BAUD_RATE):
-      ESP_LOGV(TAG, "Baud rate change command");
+
+    case CMD_SET_BAUD_RATE:
+      ESP_LOGV(TAG, "Baud rate change");
 #ifdef USE_SELECT
       if (this->baud_rate_select_ != nullptr) {
-        ESP_LOGV(TAG, "Change baud rate to %s", this->baud_rate_select_->state.c_str());
+        ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str());
       }
 #endif
       break;
-    case lowbyte(CMD_VERSION):
-      this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]);
-      ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str());
+
+    case CMD_QUERY_VERSION: {
+      std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
+      std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
+                                        this->version_[4], this->version_[3], this->version_[2]);
+      ESP_LOGV(TAG, "Firmware version: %s", version.c_str());
 #ifdef USE_TEXT_SENSOR
       if (this->version_text_sensor_ != nullptr) {
-        this->version_text_sensor_->publish_state(this->version_);
+        this->version_text_sensor_->publish_state(version);
       }
 #endif
       break;
-    case lowbyte(CMD_MAC):
-      if (len < 20) {
+    }
+
+    case CMD_QUERY_MAC_ADDRESS: {
+      if (this->buffer_pos_ < 20) {
         return false;
       }
-      this->mac_ = format_mac_address_pretty(&buffer[10]);
-      ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str());
+
+      this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
+      if (this->bluetooth_on_) {
+        std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
+      }
+
+      std::string mac_str =
+          mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
+      ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str());
 #ifdef USE_TEXT_SENSOR
       if (this->mac_text_sensor_ != nullptr) {
-        this->mac_text_sensor_->publish_state(this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_);
+        this->mac_text_sensor_->publish_state(mac_str);
       }
 #endif
 #ifdef USE_SWITCH
       if (this->bluetooth_switch_ != nullptr) {
-        this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC);
+        this->bluetooth_switch_->publish_state(this->bluetooth_on_);
       }
 #endif
       break;
-    case lowbyte(CMD_BLUETOOTH):
-      ESP_LOGV(TAG, "Bluetooth command");
+    }
+
+    case CMD_BLUETOOTH:
+      ESP_LOGV(TAG, "Bluetooth");
       break;
-    case lowbyte(CMD_SINGLE_TARGET_MODE):
-      ESP_LOGV(TAG, "Single target conf command");
+
+    case CMD_SINGLE_TARGET_MODE:
+      ESP_LOGV(TAG, "Single target conf");
 #ifdef USE_SWITCH
       if (this->multi_target_switch_ != nullptr) {
         this->multi_target_switch_->publish_state(false);
       }
 #endif
       break;
-    case lowbyte(CMD_MULTI_TARGET_MODE):
-      ESP_LOGV(TAG, "Multi target conf command");
+
+    case CMD_MULTI_TARGET_MODE:
+      ESP_LOGV(TAG, "Multi target conf");
 #ifdef USE_SWITCH
       if (this->multi_target_switch_ != nullptr) {
         this->multi_target_switch_->publish_state(true);
       }
 #endif
       break;
-    case lowbyte(CMD_QUERY_TARGET_MODE):
-      ESP_LOGV(TAG, "Query target tracking mode command");
+
+    case CMD_QUERY_TARGET_MODE:
+      ESP_LOGV(TAG, "Query target tracking mode");
 #ifdef USE_SWITCH
       if (this->multi_target_switch_ != nullptr) {
-        this->multi_target_switch_->publish_state(buffer[10] == 0x02);
+        this->multi_target_switch_->publish_state(this->buffer_data_[10] == 0x02);
       }
 #endif
       break;
-    case lowbyte(CMD_QUERY_ZONE):
-      ESP_LOGV(TAG, "Query zone conf command");
-      this->zone_type_ = std::stoi(std::to_string(buffer[10]), nullptr, 16);
+
+    case CMD_QUERY_ZONE:
+      ESP_LOGV(TAG, "Query zone conf");
+      this->zone_type_ = std::stoi(std::to_string(this->buffer_data_[10]), nullptr, 16);
       this->publish_zone_type();
 #ifdef USE_SELECT
       if (this->zone_type_select_ != nullptr) {
         ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str());
       }
 #endif
-      if (buffer[10] == 0x00) {
+      if (this->buffer_data_[10] == 0x00) {
         ESP_LOGV(TAG, "Zone: Disabled");
       }
-      if (buffer[10] == 0x01) {
+      if (this->buffer_data_[10] == 0x01) {
         ESP_LOGV(TAG, "Zone: Area detection");
       }
-      if (buffer[10] == 0x02) {
+      if (this->buffer_data_[10] == 0x02) {
         ESP_LOGV(TAG, "Zone: Area filter");
       }
-      this->process_zone_(buffer);
+      this->process_zone_();
       break;
-    case lowbyte(CMD_SET_ZONE):
-      ESP_LOGV(TAG, "Set zone conf command");
+
+    case CMD_SET_ZONE:
+      ESP_LOGV(TAG, "Set zone conf");
       this->query_zone_info();
       break;
+
     default:
       break;
   }
@@ -796,55 +834,57 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
 }
 
 // Read LD2450 buffer data
-void LD2450Component::readline_(int readch, uint8_t *buffer, uint8_t len) {
+void LD2450Component::readline_(int readch) {
   if (readch < 0) {
-    return;
+    return;  // No data available
   }
-  if (this->buffer_pos_ < len - 1) {
-    buffer[this->buffer_pos_++] = readch;
-    buffer[this->buffer_pos_] = 0;
+
+  if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) {
+    this->buffer_data_[this->buffer_pos_++] = readch;
+    this->buffer_data_[this->buffer_pos_] = 0;
   } else {
+    // We should never get here, but just in case...
+    ESP_LOGW(TAG, "Max command length exceeded; ignoring");
     this->buffer_pos_ = 0;
   }
   if (this->buffer_pos_ < 4) {
-    return;
+    return;  // Not enough data to process yet
   }
-  if (buffer[this->buffer_pos_ - 2] == 0x55 && buffer[this->buffer_pos_ - 1] == 0xCC) {
-    ESP_LOGV(TAG, "Handle periodic radar data");
-    this->handle_periodic_data_(buffer, this->buffer_pos_);
+  if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] &&
+      this->buffer_data_[this->buffer_pos_ - 1] == DATA_FRAME_FOOTER[1]) {
+    ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str());
+    this->handle_periodic_data_();
     this->buffer_pos_ = 0;  // Reset position index for next frame
-  } else if (buffer[this->buffer_pos_ - 4] == 0x04 && buffer[this->buffer_pos_ - 3] == 0x03 &&
-             buffer[this->buffer_pos_ - 2] == 0x02 && buffer[this->buffer_pos_ - 1] == 0x01) {
-    ESP_LOGV(TAG, "Handle command ack data");
-    if (this->handle_ack_data_(buffer, this->buffer_pos_)) {
-      this->buffer_pos_ = 0;  // Reset position index for next frame
+  } else if (ld2450::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
+    ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str());
+    if (this->handle_ack_data_()) {
+      this->buffer_pos_ = 0;  // Reset position index for next message
     } else {
-      ESP_LOGV(TAG, "Command ack data invalid");
+      ESP_LOGV(TAG, "Ack Data incomplete");
     }
   }
 }
 
 // Set Config Mode - Pre-requisite sending commands
 void LD2450Component::set_config_mode_(bool enable) {
-  uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
-  uint8_t cmd_value[2] = {0x01, 0x00};
-  this->send_command_(cmd, enable ? cmd_value : nullptr, 2);
+  const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
+  const uint8_t cmd_value[2] = {0x01, 0x00};
+  this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value));
 }
 
 // Set Bluetooth Enable/Disable
 void LD2450Component::set_bluetooth(bool enable) {
   this->set_config_mode_(true);
-  uint8_t enable_cmd_value[2] = {0x01, 0x00};
-  uint8_t disable_cmd_value[2] = {0x00, 0x00};
-  this->send_command_(CMD_BLUETOOTH, enable ? enable_cmd_value : disable_cmd_value, 2);
+  const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00};
+  this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value));
   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
 }
 
 // Set Baud rate
 void LD2450Component::set_baud_rate(const std::string &state) {
   this->set_config_mode_(true);
-  uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
-  this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2);
+  const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
+  this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
   this->set_timeout(200, [this]() { this->restart_(); });
 }
 
@@ -885,12 +925,12 @@ void LD2450Component::factory_reset() {
 void LD2450Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); }
 
 // Get LD2450 firmware version
-void LD2450Component::get_version_() { this->send_command_(CMD_VERSION, nullptr, 0); }
+void LD2450Component::get_version_() { this->send_command_(CMD_QUERY_VERSION, nullptr, 0); }
 
 // Get LD2450 mac address
 void LD2450Component::get_mac_() {
   uint8_t cmd_value[2] = {0x01, 0x00};
-  this->send_command_(CMD_MAC, cmd_value, 2);
+  this->send_command_(CMD_QUERY_MAC_ADDRESS, cmd_value, 2);
 }
 
 // Query for target tracking mode
diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h
index 5ddccab638..ae72a0d8cb 100644
--- a/esphome/components/ld2450/ld2450.h
+++ b/esphome/components/ld2450/ld2450.h
@@ -38,10 +38,18 @@ namespace ld2450 {
 
 // Constants
 static const uint8_t DEFAULT_PRESENCE_TIMEOUT = 5;  // Timeout to reset presense status 5 sec.
-static const uint8_t MAX_LINE_LENGTH = 60;          // Max characters for serial buffer
+static const uint8_t MAX_LINE_LENGTH = 41;          // Max characters for serial buffer
 static const uint8_t MAX_TARGETS = 3;               // Max 3 Targets in LD2450
 static const uint8_t MAX_ZONES = 3;                 // Max 3 Zones in LD2450
 
+enum Direction : uint8_t {
+  DIRECTION_APPROACHING = 0,
+  DIRECTION_MOVING_AWAY = 1,
+  DIRECTION_STATIONARY = 2,
+  DIRECTION_NA = 3,
+  DIRECTION_UNDEFINED = 4,
+};
+
 // Target coordinate struct
 struct Target {
   int16_t x;
@@ -67,19 +75,22 @@ struct ZoneOfNumbers {
 #endif
 
 class LD2450Component : public Component, public uart::UARTDevice {
-#ifdef USE_SENSOR
-  SUB_SENSOR(target_count)
-  SUB_SENSOR(still_target_count)
-  SUB_SENSOR(moving_target_count)
-#endif
 #ifdef USE_BINARY_SENSOR
-  SUB_BINARY_SENSOR(target)
   SUB_BINARY_SENSOR(moving_target)
   SUB_BINARY_SENSOR(still_target)
+  SUB_BINARY_SENSOR(target)
+#endif
+#ifdef USE_SENSOR
+  SUB_SENSOR(moving_target_count)
+  SUB_SENSOR(still_target_count)
+  SUB_SENSOR(target_count)
 #endif
 #ifdef USE_TEXT_SENSOR
-  SUB_TEXT_SENSOR(version)
   SUB_TEXT_SENSOR(mac)
+  SUB_TEXT_SENSOR(version)
+#endif
+#ifdef USE_NUMBER
+  SUB_NUMBER(presence_timeout)
 #endif
 #ifdef USE_SELECT
   SUB_SELECT(baud_rate)
@@ -90,12 +101,9 @@ class LD2450Component : public Component, public uart::UARTDevice {
   SUB_SWITCH(multi_target)
 #endif
 #ifdef USE_BUTTON
-  SUB_BUTTON(reset)
+  SUB_BUTTON(factory_reset)
   SUB_BUTTON(restart)
 #endif
-#ifdef USE_NUMBER
-  SUB_NUMBER(presence_timeout)
-#endif
 
  public:
   void setup() override;
@@ -138,10 +146,10 @@ class LD2450Component : public Component, public uart::UARTDevice {
  protected:
   void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
   void set_config_mode_(bool enable);
-  void handle_periodic_data_(uint8_t *buffer, uint8_t len);
-  bool handle_ack_data_(uint8_t *buffer, uint8_t len);
-  void process_zone_(uint8_t *buffer);
-  void readline_(int readch, uint8_t *buffer, uint8_t len);
+  void handle_periodic_data_();
+  bool handle_ack_data_();
+  void process_zone_();
+  void readline_(int readch);
   void get_version_();
   void get_mac_();
   void query_target_tracking_mode_();
@@ -159,13 +167,14 @@ class LD2450Component : public Component, public uart::UARTDevice {
   uint32_t moving_presence_millis_ = 0;
   uint16_t throttle_ = 0;
   uint16_t timeout_ = 5;
-  uint8_t buffer_pos_ = 0;  // where to resume processing/populating buffer
   uint8_t buffer_data_[MAX_LINE_LENGTH];
+  uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0};
+  uint8_t version_[6] = {0, 0, 0, 0, 0, 0};
+  uint8_t buffer_pos_ = 0;  // where to resume processing/populating buffer
   uint8_t zone_type_ = 0;
+  bool bluetooth_on_{false};
   Target target_info_[MAX_TARGETS];
   Zone zone_config_[MAX_ZONES];
-  std::string version_{};
-  std::string mac_{};
 
   // Change detection - cache previous values to avoid redundant publishes
   // All values are initialized to sentinel values that are outside the valid sensor ranges
@@ -176,8 +185,8 @@ class LD2450Component : public Component, public uart::UARTDevice {
     int16_t speed = std::numeric_limits::min();         // -32768, outside practical sensor range
     uint16_t resolution = std::numeric_limits::max();  // 65535, unlikely resolution value
     uint16_t distance = std::numeric_limits::max();    // 65535, outside range of 0 to ~8990
+    Direction direction = DIRECTION_UNDEFINED;                   // Undefined, will differ from any real direction
     float angle = NAN;                                           // NAN, safe sentinel for floats
-    std::string direction = "";                                  // Empty string, will differ from any real direction
   } cached_target_data_[MAX_TARGETS];
 
   struct CachedZoneData {

From c6f7e842567e87fe5e074b21b38577946ce8fba1 Mon Sep 17 00:00:00 2001
From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com>
Date: Mon, 7 Jul 2025 14:30:34 +0200
Subject: [PATCH 256/293] [nextion] Review `touch_sleep_timeout` (#9345)

---
 esphome/components/nextion/display.py         |  4 ++-
 esphome/components/nextion/nextion.cpp        | 26 ++++++++--------
 esphome/components/nextion/nextion.h          | 31 ++++++++++++++++---
 .../components/nextion/nextion_commands.cpp   | 13 ++++----
 4 files changed, 49 insertions(+), 25 deletions(-)

diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py
index 3ec80a6bcc..4b40c34001 100644
--- a/esphome/components/nextion/display.py
+++ b/esphome/components/nextion/display.py
@@ -95,7 +95,9 @@ CONFIG_SCHEMA = (
             cv.Optional(CONF_SKIP_CONNECTION_HANDSHAKE, default=False): cv.boolean,
             cv.Optional(CONF_START_UP_PAGE): cv.uint8_t,
             cv.Optional(CONF_TFT_URL): cv.url,
-            cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.int_range(min=3, max=65535),
+            cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.Any(
+                0, cv.int_range(min=3, max=65535)
+            ),
             cv.Optional(CONF_WAKE_UP_PAGE): cv.uint8_t,
         }
     )
diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp
index ed23ffe2c3..54a35a061d 100644
--- a/esphome/components/nextion/nextion.cpp
+++ b/esphome/components/nextion/nextion.cpp
@@ -13,14 +13,11 @@ void Nextion::setup() {
   this->is_setup_ = false;
   this->connection_state_.ignore_is_setup_ = true;
 
-  // Wake up the nextion
-  this->send_command_("bkcmd=0");
-  this->send_command_("sleep=0");
+  // Wake up the nextion and ensure clean communication state
+  this->send_command_("sleep=0");  // Exit sleep mode if sleeping
+  this->send_command_("bkcmd=0");  // Disable return data during init sequence
 
-  this->send_command_("bkcmd=0");
-  this->send_command_("sleep=0");
-
-  // Reboot it
+  // Reset device for clean state - critical for reliable communication
   this->send_command_("rest");
 
   this->connection_state_.ignore_is_setup_ = false;
@@ -140,6 +137,7 @@ void Nextion::reset_(bool reset_nextion) {
 
 void Nextion::dump_config() {
   ESP_LOGCONFIG(TAG, "Nextion:");
+
 #ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
   ESP_LOGCONFIG(TAG, "  Skip handshake: YES");
 #else  // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
@@ -151,19 +149,17 @@ void Nextion::dump_config() {
 #ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
                 "  Exit reparse:   YES\n"
 #endif  // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
-                "  Wake On Touch:  %s",
+                "  Wake On Touch:  %s\n"
+                "  Touch Timeout:  %" PRIu16,
                 this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(),
-                this->flash_size_.c_str(), YESNO(this->auto_wake_on_touch_));
+                this->flash_size_.c_str(), YESNO(this->connection_state_.auto_wake_on_touch_),
+                this->touch_sleep_timeout_);
 #endif  // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
 
 #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
   ESP_LOGCONFIG(TAG, "  Max commands per loop: %u", this->max_commands_per_loop_);
 #endif  // USE_NEXTION_MAX_COMMANDS_PER_LOOP
 
-  if (this->touch_sleep_timeout_ != 0) {
-    ESP_LOGCONFIG(TAG, "  Touch Timeout:  %" PRIu16, this->touch_sleep_timeout_);
-  }
-
   if (this->wake_up_page_ != 255) {
     ESP_LOGCONFIG(TAG, "  Wake Up Page:   %u", this->wake_up_page_);
   }
@@ -311,6 +307,10 @@ void Nextion::loop() {
       this->set_wake_up_page(this->wake_up_page_);
     }
 
+    if (this->touch_sleep_timeout_ != 0) {
+      this->set_touch_sleep_timeout(this->touch_sleep_timeout_);
+    }
+
     this->connection_state_.ignore_is_setup_ = false;
   }
 
diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h
index efa607ef8e..76f6860962 100644
--- a/esphome/components/nextion/nextion.h
+++ b/esphome/components/nextion/nextion.h
@@ -1164,18 +1164,39 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
   void update_components_by_prefix(const std::string &prefix);
 
   /**
-   * Set the touch sleep timeout of the display.
-   * @param timeout Timeout in seconds.
+   * Set the touch sleep timeout of the display using the `thsp` command.
+   *
+   * Sets internal No-touch-then-sleep timer to specified value in seconds.
+   * Nextion will auto-enter sleep mode if and when this timer expires.
+   *
+   * @param touch_sleep_timeout Timeout in seconds.
+   *                           Range: 3 to 65535 seconds (minimum 3 seconds, maximum ~18 hours 12 minutes 15 seconds)
+   *                           Use 0 to disable touch sleep timeout.
+   *
+   * @note Once `thsp` is set, it will persist until reboot or reset. The Nextion device
+   *       needs to exit sleep mode to issue `thsp=0` to disable sleep on no touch.
+   *
+   * @note The display will only wake up by a restart or by setting up `thup` (auto wake on touch).
+   *       See set_auto_wake_on_touch() to configure wake behavior.
    *
    * Example:
    * ```cpp
+   * // Set 30 second touch timeout
    * it.set_touch_sleep_timeout(30);
+   *
+   * // Set maximum timeout (~18 hours)
+   * it.set_touch_sleep_timeout(65535);
+   *
+   * // Disable touch sleep timeout
+   * it.set_touch_sleep_timeout(0);
    * ```
    *
-   * After 30 seconds the display will go to sleep. Note: the display will only wakeup by a restart or by setting up
-   * `thup`.
+   * Related Nextion instruction: `thsp=`
+   *
+   * @see set_auto_wake_on_touch() Configure automatic wake on touch
+   * @see sleep() Manually control sleep state
    */
-  void set_touch_sleep_timeout(uint16_t touch_sleep_timeout);
+  void set_touch_sleep_timeout(uint16_t touch_sleep_timeout = 0);
 
   /**
    * Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode.
diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp
index 018f8fe732..f3a282717b 100644
--- a/esphome/components/nextion/nextion_commands.cpp
+++ b/esphome/components/nextion/nextion_commands.cpp
@@ -15,14 +15,15 @@ void Nextion::set_wake_up_page(uint8_t wake_up_page) {
   this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true);
 }
 
-void Nextion::set_touch_sleep_timeout(uint16_t touch_sleep_timeout) {
-  if (touch_sleep_timeout < 3) {
-    ESP_LOGD(TAG, "Sleep timeout out of bounds (3-65535)");
-    return;
+void Nextion::set_touch_sleep_timeout(const uint16_t touch_sleep_timeout) {
+  // Validate range: Nextion thsp command requires min 3, max 65535 seconds (0 disables)
+  if (touch_sleep_timeout != 0 && touch_sleep_timeout < 3) {
+    this->touch_sleep_timeout_ = 3;  // Auto-correct to minimum valid value
+  } else {
+    this->touch_sleep_timeout_ = touch_sleep_timeout;
   }
 
-  this->touch_sleep_timeout_ = touch_sleep_timeout;
-  this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", touch_sleep_timeout, true);
+  this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", this->touch_sleep_timeout_, true);
 }
 
 void Nextion::sleep(bool sleep) {

From 8147d117a0bac83c3625bb3697ac069dc436d077 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Tue, 8 Jul 2025 03:55:02 +1200
Subject: [PATCH 257/293] [core] Move platform helper implementations into
 their own file (#9361)

---
 esphome/components/esp32/helpers.cpp     |  69 ++++++++
 esphome/components/esp8266/helpers.cpp   |  31 ++++
 esphome/components/host/helpers.cpp      |  57 +++++++
 esphome/components/libretiny/helpers.cpp |  35 ++++
 esphome/components/rp2040/helpers.cpp    |  55 +++++++
 esphome/core/helpers.cpp                 | 193 +----------------------
 script/ci-custom.py                      |   6 +
 7 files changed, 255 insertions(+), 191 deletions(-)
 create mode 100644 esphome/components/esp32/helpers.cpp
 create mode 100644 esphome/components/esp8266/helpers.cpp
 create mode 100644 esphome/components/host/helpers.cpp
 create mode 100644 esphome/components/libretiny/helpers.cpp
 create mode 100644 esphome/components/rp2040/helpers.cpp

diff --git a/esphome/components/esp32/helpers.cpp b/esphome/components/esp32/helpers.cpp
new file mode 100644
index 0000000000..310e7bd94a
--- /dev/null
+++ b/esphome/components/esp32/helpers.cpp
@@ -0,0 +1,69 @@
+#include "esphome/core/helpers.h"
+
+#ifdef USE_ESP32
+
+#include "esp_efuse.h"
+#include "esp_efuse_table.h"
+#include "esp_mac.h"
+
+#include 
+#include 
+#include "esp_random.h"
+#include "esp_system.h"
+
+namespace esphome {
+
+uint32_t random_uint32() { return esp_random(); }
+bool random_bytes(uint8_t *data, size_t len) {
+  esp_fill_random(data, len);
+  return true;
+}
+
+Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); }
+Mutex::~Mutex() {}
+void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
+bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
+void Mutex::unlock() { xSemaphoreGive(this->handle_); }
+
+// only affects the executing core
+// so should not be used as a mutex lock, only to get accurate timing
+IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
+IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
+
+void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
+#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
+  // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default
+  // returns the 802.15.4 EUI-64 address, so we read directly from eFuse instead.
+  if (has_custom_mac_address()) {
+    esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48);
+  } else {
+    esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48);
+  }
+#else
+  if (has_custom_mac_address()) {
+    esp_efuse_mac_get_custom(mac);
+  } else {
+    esp_efuse_mac_get_default(mac);
+  }
+#endif
+}
+
+void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); }
+
+bool has_custom_mac_address() {
+#if !defined(USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC)
+  uint8_t mac[6];
+  // do not use 'esp_efuse_mac_get_custom(mac)' because it drops an error in the logs whenever it fails
+#ifndef USE_ESP32_VARIANT_ESP32
+  return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac);
+#else
+  return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac);
+#endif
+#else
+  return false;
+#endif
+}
+
+}  // namespace esphome
+
+#endif  // USE_ESP32
diff --git a/esphome/components/esp8266/helpers.cpp b/esphome/components/esp8266/helpers.cpp
new file mode 100644
index 0000000000..993de710c6
--- /dev/null
+++ b/esphome/components/esp8266/helpers.cpp
@@ -0,0 +1,31 @@
+#include "esphome/core/helpers.h"
+
+#ifdef USE_ESP8266
+
+#include 
+#include 
+// for xt_rsil()/xt_wsr_ps()
+#include 
+
+namespace esphome {
+
+uint32_t random_uint32() { return os_random(); }
+bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; }
+
+// ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS.
+Mutex::Mutex() {}
+Mutex::~Mutex() {}
+void Mutex::lock() {}
+bool Mutex::try_lock() { return true; }
+void Mutex::unlock() {}
+
+IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
+IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
+
+void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
+  wifi_get_macaddr(STATION_IF, mac);
+}
+
+}  // namespace esphome
+
+#endif  // USE_ESP8266
diff --git a/esphome/components/host/helpers.cpp b/esphome/components/host/helpers.cpp
new file mode 100644
index 0000000000..fdad4f5cb6
--- /dev/null
+++ b/esphome/components/host/helpers.cpp
@@ -0,0 +1,57 @@
+#include "esphome/core/helpers.h"
+
+#ifdef USE_HOST
+
+#ifndef _WIN32
+#include 
+#include 
+#include 
+#endif
+#include 
+#include 
+#include 
+
+#include "esphome/core/defines.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+
+static const char *const TAG = "helpers.host";
+
+uint32_t random_uint32() {
+  std::random_device dev;
+  std::mt19937 rng(dev());
+  std::uniform_int_distribution dist(0, std::numeric_limits::max());
+  return dist(rng);
+}
+
+bool random_bytes(uint8_t *data, size_t len) {
+  FILE *fp = fopen("/dev/urandom", "r");
+  if (fp == nullptr) {
+    ESP_LOGW(TAG, "Could not open /dev/urandom, errno=%d", errno);
+    exit(1);
+  }
+  size_t read = fread(data, 1, len, fp);
+  if (read != len) {
+    ESP_LOGW(TAG, "Not enough data from /dev/urandom");
+    exit(1);
+  }
+  fclose(fp);
+  return true;
+}
+
+// Host platform uses std::mutex for proper thread synchronization
+Mutex::Mutex() { handle_ = new std::mutex(); }
+Mutex::~Mutex() { delete static_cast(handle_); }
+void Mutex::lock() { static_cast(handle_)->lock(); }
+bool Mutex::try_lock() { return static_cast(handle_)->try_lock(); }
+void Mutex::unlock() { static_cast(handle_)->unlock(); }
+
+void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
+  static const uint8_t esphome_host_mac_address[6] = USE_ESPHOME_HOST_MAC_ADDRESS;
+  memcpy(mac, esphome_host_mac_address, sizeof(esphome_host_mac_address));
+}
+
+}  // namespace esphome
+
+#endif  // USE_HOST
diff --git a/esphome/components/libretiny/helpers.cpp b/esphome/components/libretiny/helpers.cpp
new file mode 100644
index 0000000000..b6451860d5
--- /dev/null
+++ b/esphome/components/libretiny/helpers.cpp
@@ -0,0 +1,35 @@
+#include "esphome/core/helpers.h"
+
+#ifdef USE_LIBRETINY
+
+#include "esphome/core/hal.h"
+
+#include   // for macAddress()
+
+namespace esphome {
+
+uint32_t random_uint32() { return rand(); }
+
+bool random_bytes(uint8_t *data, size_t len) {
+  lt_rand_bytes(data, len);
+  return true;
+}
+
+Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); }
+Mutex::~Mutex() {}
+void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
+bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
+void Mutex::unlock() { xSemaphoreGive(this->handle_); }
+
+// only affects the executing core
+// so should not be used as a mutex lock, only to get accurate timing
+IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
+IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
+
+void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
+  WiFi.macAddress(mac);
+}
+
+}  // namespace esphome
+
+#endif  // USE_LIBRETINY
diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2040/helpers.cpp
new file mode 100644
index 0000000000..a6eac58dc6
--- /dev/null
+++ b/esphome/components/rp2040/helpers.cpp
@@ -0,0 +1,55 @@
+#include "esphome/core/helpers.h"
+#include "esphome/core/defines.h"
+
+#ifdef USE_RP2040
+
+#include "esphome/core/hal.h"
+
+#if defined(USE_WIFI)
+#include 
+#endif
+#include 
+#include 
+
+namespace esphome {
+
+uint32_t random_uint32() {
+  uint32_t result = 0;
+  for (uint8_t i = 0; i < 32; i++) {
+    result <<= 1;
+    result |= rosc_hw->randombit;
+  }
+  return result;
+}
+
+bool random_bytes(uint8_t *data, size_t len) {
+  while (len-- != 0) {
+    uint8_t result = 0;
+    for (uint8_t i = 0; i < 8; i++) {
+      result <<= 1;
+      result |= rosc_hw->randombit;
+    }
+    *data++ = result;
+  }
+  return true;
+}
+
+// RP2040 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS.
+Mutex::Mutex() {}
+Mutex::~Mutex() {}
+void Mutex::lock() {}
+bool Mutex::try_lock() { return true; }
+void Mutex::unlock() {}
+
+IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); }
+IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
+
+void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
+#ifdef USE_WIFI
+  WiFi.macAddress(mac);
+#endif
+}
+
+}  // namespace esphome
+
+#endif  // USE_RP2040
diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp
index 7d9b86fccd..72722169d4 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -12,47 +12,10 @@
 #include 
 #include 
 
-#ifdef USE_HOST
-#ifndef _WIN32
-#include 
-#include 
-#include 
-#endif
-#include 
-#endif
-#if defined(USE_ESP8266)
-#include 
-#include 
-// for xt_rsil()/xt_wsr_ps()
-#include 
-#elif defined(USE_ESP32_FRAMEWORK_ARDUINO)
-#include 
-#elif defined(USE_ESP_IDF)
-#include 
-#include 
-#include "esp_random.h"
-#include "esp_system.h"
-#elif defined(USE_RP2040)
-#if defined(USE_WIFI)
-#include 
-#endif
-#include 
-#include 
-#elif defined(USE_HOST)
-#include 
-#include 
-#endif
 #ifdef USE_ESP32
-#include "esp_efuse.h"
-#include "esp_efuse_table.h"
-#include "esp_mac.h"
 #include "rom/crc.h"
 #endif
 
-#ifdef USE_LIBRETINY
-#include   // for macAddress()
-#endif
-
 namespace esphome {
 
 static const char *const TAG = "helpers";
@@ -177,70 +140,7 @@ uint32_t fnv1_hash(const std::string &str) {
   return hash;
 }
 
-#ifdef USE_ESP32
-uint32_t random_uint32() { return esp_random(); }
-#elif defined(USE_ESP8266)
-uint32_t random_uint32() { return os_random(); }
-#elif defined(USE_RP2040)
-uint32_t random_uint32() {
-  uint32_t result = 0;
-  for (uint8_t i = 0; i < 32; i++) {
-    result <<= 1;
-    result |= rosc_hw->randombit;
-  }
-  return result;
-}
-#elif defined(USE_LIBRETINY)
-uint32_t random_uint32() { return rand(); }
-#elif defined(USE_HOST)
-uint32_t random_uint32() {
-  std::random_device dev;
-  std::mt19937 rng(dev());
-  std::uniform_int_distribution dist(0, std::numeric_limits::max());
-  return dist(rng);
-}
-#endif
 float random_float() { return static_cast(random_uint32()) / static_cast(UINT32_MAX); }
-#ifdef USE_ESP32
-bool random_bytes(uint8_t *data, size_t len) {
-  esp_fill_random(data, len);
-  return true;
-}
-#elif defined(USE_ESP8266)
-bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; }
-#elif defined(USE_RP2040)
-bool random_bytes(uint8_t *data, size_t len) {
-  while (len-- != 0) {
-    uint8_t result = 0;
-    for (uint8_t i = 0; i < 8; i++) {
-      result <<= 1;
-      result |= rosc_hw->randombit;
-    }
-    *data++ = result;
-  }
-  return true;
-}
-#elif defined(USE_LIBRETINY)
-bool random_bytes(uint8_t *data, size_t len) {
-  lt_rand_bytes(data, len);
-  return true;
-}
-#elif defined(USE_HOST)
-bool random_bytes(uint8_t *data, size_t len) {
-  FILE *fp = fopen("/dev/urandom", "r");
-  if (fp == nullptr) {
-    ESP_LOGW(TAG, "Could not open /dev/urandom, errno=%d", errno);
-    exit(1);
-  }
-  size_t read = fread(data, 1, len, fp);
-  if (read != len) {
-    ESP_LOGW(TAG, "Not enough data from /dev/urandom");
-    exit(1);
-  }
-  fclose(fp);
-  return true;
-}
-#endif
 
 // Strings
 
@@ -644,42 +544,6 @@ void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green
   blue += delta;
 }
 
-// System APIs
-#if defined(USE_ESP8266) || defined(USE_RP2040)
-// ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS.
-Mutex::Mutex() {}
-Mutex::~Mutex() {}
-void Mutex::lock() {}
-bool Mutex::try_lock() { return true; }
-void Mutex::unlock() {}
-#elif defined(USE_ESP32) || defined(USE_LIBRETINY)
-Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); }
-Mutex::~Mutex() {}
-void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
-bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
-void Mutex::unlock() { xSemaphoreGive(this->handle_); }
-#elif defined(USE_HOST)
-// Host platform uses std::mutex for proper thread synchronization
-Mutex::Mutex() { handle_ = new std::mutex(); }
-Mutex::~Mutex() { delete static_cast(handle_); }
-void Mutex::lock() { static_cast(handle_)->lock(); }
-bool Mutex::try_lock() { return static_cast(handle_)->try_lock(); }
-void Mutex::unlock() { static_cast(handle_)->unlock(); }
-#endif
-
-#if defined(USE_ESP8266)
-IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
-IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
-#elif defined(USE_ESP32) || defined(USE_LIBRETINY)
-// only affects the executing core
-// so should not be used as a mutex lock, only to get accurate timing
-IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
-IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
-#elif defined(USE_RP2040)
-IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); }
-IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
-#endif
-
 uint8_t HighFrequencyLoopRequester::num_requests = 0;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 void HighFrequencyLoopRequester::start() {
   if (this->started_)
@@ -695,45 +559,6 @@ void HighFrequencyLoopRequester::stop() {
 }
 bool HighFrequencyLoopRequester::is_high_frequency() { return num_requests > 0; }
 
-#if defined(USE_HOST)
-void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
-  static const uint8_t esphome_host_mac_address[6] = USE_ESPHOME_HOST_MAC_ADDRESS;
-  memcpy(mac, esphome_host_mac_address, sizeof(esphome_host_mac_address));
-}
-#elif defined(USE_ESP32)
-void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
-#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
-  // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default
-  // returns the 802.15.4 EUI-64 address, so we read directly from eFuse instead.
-  if (has_custom_mac_address()) {
-    esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48);
-  } else {
-    esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48);
-  }
-#else
-  if (has_custom_mac_address()) {
-    esp_efuse_mac_get_custom(mac);
-  } else {
-    esp_efuse_mac_get_default(mac);
-  }
-#endif
-}
-#elif defined(USE_ESP8266)
-void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
-  wifi_get_macaddr(STATION_IF, mac);
-}
-#elif defined(USE_RP2040)
-void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
-#ifdef USE_WIFI
-  WiFi.macAddress(mac);
-#endif
-}
-#elif defined(USE_LIBRETINY)
-void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
-  WiFi.macAddress(mac);
-}
-#endif
-
 std::string get_mac_address() {
   uint8_t mac[6];
   get_mac_address_raw(mac);
@@ -746,24 +571,10 @@ std::string get_mac_address_pretty() {
   return format_mac_address_pretty(mac);
 }
 
-#ifdef USE_ESP32
-void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); }
+#ifndef USE_ESP32
+bool has_custom_mac_address() { return false; }
 #endif
 
-bool has_custom_mac_address() {
-#if defined(USE_ESP32) && !defined(USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC)
-  uint8_t mac[6];
-  // do not use 'esp_efuse_mac_get_custom(mac)' because it drops an error in the logs whenever it fails
-#ifndef USE_ESP32_VARIANT_ESP32
-  return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac);
-#else
-  return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac);
-#endif
-#else
-  return false;
-#endif
-}
-
 bool mac_address_is_valid(const uint8_t *mac) {
   bool is_all_zeros = true;
   bool is_all_ones = true;
diff --git a/script/ci-custom.py b/script/ci-custom.py
index fbabbc1e74..d0b518251f 100755
--- a/script/ci-custom.py
+++ b/script/ci-custom.py
@@ -559,6 +559,12 @@ def lint_relative_py_import(fname):
         "esphome/components/libretiny/core.cpp",
         "esphome/components/host/core.cpp",
         "esphome/components/zephyr/core.cpp",
+        "esphome/components/esp32/helpers.cpp",
+        "esphome/components/esp8266/helpers.cpp",
+        "esphome/components/rp2040/helpers.cpp",
+        "esphome/components/libretiny/helpers.cpp",
+        "esphome/components/host/helpers.cpp",
+        "esphome/components/zephyr/helpers.cpp",
         "esphome/components/http_request/httplib.h",
     ],
 )

From e88b8d10ece6d91d8abc827c7c8f3c92172c73cb Mon Sep 17 00:00:00 2001
From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com>
Date: Mon, 7 Jul 2025 19:04:01 +0200
Subject: [PATCH 258/293] [nextion] Add optional device info storage
 configuration (#9366)

---
 esphome/components/nextion/base_component.py |  1 +
 esphome/components/nextion/display.py        |  5 +++++
 esphome/components/nextion/nextion.cpp       | 18 +++++++++++++++---
 esphome/components/nextion/nextion.h         |  2 ++
 4 files changed, 23 insertions(+), 3 deletions(-)

diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py
index 98dea4b513..392481e39a 100644
--- a/esphome/components/nextion/base_component.py
+++ b/esphome/components/nextion/base_component.py
@@ -11,6 +11,7 @@ CONF_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch"
 CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color"
 CONF_COMMAND_SPACING = "command_spacing"
 CONF_COMPONENT_NAME = "component_name"
+CONF_DUMP_DEVICE_INFO = "dump_device_info"
 CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start"
 CONF_FONT_ID = "font_id"
 CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color"
diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py
index 4b40c34001..4254ae45fe 100644
--- a/esphome/components/nextion/display.py
+++ b/esphome/components/nextion/display.py
@@ -15,6 +15,7 @@ from . import Nextion, nextion_ns, nextion_ref
 from .base_component import (
     CONF_AUTO_WAKE_ON_TOUCH,
     CONF_COMMAND_SPACING,
+    CONF_DUMP_DEVICE_INFO,
     CONF_EXIT_REPARSE_ON_START,
     CONF_MAX_COMMANDS_PER_LOOP,
     CONF_MAX_QUEUE_SIZE,
@@ -57,6 +58,7 @@ CONFIG_SCHEMA = (
                 cv.positive_time_period_milliseconds,
                 cv.Range(max=TimePeriod(milliseconds=255)),
             ),
+            cv.Optional(CONF_DUMP_DEVICE_INFO, default=False): cv.boolean,
             cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean,
             cv.Optional(CONF_MAX_COMMANDS_PER_LOOP): cv.uint16_t,
             cv.Optional(CONF_MAX_QUEUE_SIZE): cv.positive_int,
@@ -174,6 +176,9 @@ async def to_code(config):
 
     cg.add(var.set_auto_wake_on_touch(config[CONF_AUTO_WAKE_ON_TOUCH]))
 
+    if config[CONF_DUMP_DEVICE_INFO]:
+        cg.add_define("USE_NEXTION_CONFIG_DUMP_DEVICE_INFO")
+
     if config[CONF_EXIT_REPARSE_ON_START]:
         cg.add_define("USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START")
 
diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp
index 54a35a061d..66e2d26061 100644
--- a/esphome/components/nextion/nextion.cpp
+++ b/esphome/components/nextion/nextion.cpp
@@ -110,11 +110,19 @@ bool Nextion::check_connect_() {
   this->is_detected_ = (connect_info.size() == 7);
   if (this->is_detected_) {
     ESP_LOGN(TAG, "Connect info: %zu", connect_info.size());
-
+#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
     this->device_model_ = connect_info[2];
     this->firmware_version_ = connect_info[3];
     this->serial_number_ = connect_info[5];
     this->flash_size_ = connect_info[6];
+#else   // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
+    ESP_LOGI(TAG,
+             "  Device Model:   %s\n"
+             "  FW Version:     %s\n"
+             "  Serial Number:  %s\n"
+             "  Flash Size:     %s\n",
+             connect_info[2].c_str(), connect_info[3].c_str(), connect_info[5].c_str(), connect_info[6].c_str());
+#endif  // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
   } else {
     ESP_LOGE(TAG, "Bad connect value: '%s'", response.c_str());
   }
@@ -142,18 +150,22 @@ void Nextion::dump_config() {
   ESP_LOGCONFIG(TAG, "  Skip handshake: YES");
 #else  // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
   ESP_LOGCONFIG(TAG,
+#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
                 "  Device Model:   %s\n"
                 "  FW Version:     %s\n"
                 "  Serial Number:  %s\n"
                 "  Flash Size:     %s\n"
+#endif  // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
 #ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
                 "  Exit reparse:   YES\n"
 #endif  // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
                 "  Wake On Touch:  %s\n"
                 "  Touch Timeout:  %" PRIu16,
+#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
                 this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(),
-                this->flash_size_.c_str(), YESNO(this->connection_state_.auto_wake_on_touch_),
-                this->touch_sleep_timeout_);
+                this->flash_size_.c_str(),
+#endif  // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
+                YESNO(this->connection_state_.auto_wake_on_touch_), this->touch_sleep_timeout_);
 #endif  // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
 
 #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h
index 76f6860962..e2c4faa1d0 100644
--- a/esphome/components/nextion/nextion.h
+++ b/esphome/components/nextion/nextion.h
@@ -1457,10 +1457,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
   optional writer_;
   optional brightness_;
 
+#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
   std::string device_model_;
   std::string firmware_version_;
   std::string serial_number_;
   std::string flash_size_;
+#endif  // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
 
   void remove_front_no_sensors_();
 

From 138ff749f352323c79d55d2e714fdf717dc327d7 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 14:34:12 -0500
Subject: [PATCH 259/293] Optimize Bluetooth proxy batching and increase scan
 buffer capacity (#9328)

---
 esphome/components/bluetooth_proxy/bluetooth_proxy.cpp | 7 ++++++-
 esphome/components/esp32_ble/ble.h                     | 9 +++++++--
 2 files changed, 13 insertions(+), 3 deletions(-)

diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp
index bf0adf1efd..98f11fac7a 100644
--- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp
+++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp
@@ -52,7 +52,12 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
   return true;
 }
 
-static constexpr size_t FLUSH_BATCH_SIZE = 8;
+// Batch size for BLE advertisements to maximize WiFi efficiency
+// Each advertisement is up to 80 bytes when packaged (including protocol overhead)
+// Most advertisements are 20-30 bytes, allowing even more to fit per packet
+// 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload
+// This achieves ~97% WiFi MTU utilization while staying under the limit
+static constexpr size_t FLUSH_BATCH_SIZE = 16;
 static std::vector &get_batch_buffer() {
   static std::vector batch_buffer;
   return batch_buffer;
diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h
index 81582eb09a..2c5697df82 100644
--- a/esphome/components/esp32_ble/ble.h
+++ b/esphome/components/esp32_ble/ble.h
@@ -25,10 +25,15 @@ namespace esphome {
 namespace esp32_ble {
 
 // Maximum number of BLE scan results to buffer
+// Sized to handle bursts of advertisements while allowing for processing delays
+// With 16 advertisements per batch and some safety margin:
+// - Without PSRAM: 24 entries (1.5× batch size)
+// - With PSRAM: 36 entries (2.25× batch size)
+// The reduced structure size (~80 bytes vs ~400 bytes) allows for larger buffers
 #ifdef USE_PSRAM
-static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 32;
+static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 36;
 #else
-static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 20;
+static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 24;
 #endif
 
 // Maximum size of the BLE event queue - must be power of 2 for lock-free queue

From 3ef392d43384b766ada29d67e543b01e3b2f04ff Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 14:57:55 -0500
Subject: [PATCH 260/293] Fix scheduler race conditions and add comprehensive
 test suite (#9348)

---
 esphome/core/scheduler.cpp                    | 191 ++++++------
 esphome/core/scheduler.h                      |  54 +++-
 .../__init__.py                               |  21 ++
 .../scheduler_bulk_cleanup_component.cpp      |  72 +++++
 .../scheduler_bulk_cleanup_component.h        |  18 ++
 .../__init__.py                               |  21 ++
 .../heap_scheduler_stress_component.cpp       | 104 +++++++
 .../heap_scheduler_stress_component.h         |  22 ++
 .../__init__.py                               |  21 ++
 .../rapid_cancellation_component.cpp          |  80 +++++
 .../rapid_cancellation_component.h            |  22 ++
 .../__init__.py                               |  21 ++
 .../recursive_timeout_component.cpp           |  40 +++
 .../recursive_timeout_component.h             |  20 ++
 .../__init__.py                               |  23 ++
 .../simultaneous_callbacks_component.cpp      | 109 +++++++
 .../simultaneous_callbacks_component.h        |  24 ++
 .../__init__.py                               |  21 ++
 .../string_lifetime_component.cpp             | 275 ++++++++++++++++++
 .../string_lifetime_component.h               |  37 +++
 .../__init__.py                               |  21 ++
 .../string_name_stress_component.cpp          | 110 +++++++
 .../string_name_stress_component.h            |  22 ++
 .../fixtures/scheduler_bulk_cleanup.yaml      |  23 ++
 .../fixtures/scheduler_defer_cancel.yaml      |  51 ++++
 .../scheduler_defer_cancels_regular.yaml      |  34 +++
 ....yaml => scheduler_defer_fifo_simple.yaml} |   2 +-
 ...tress.yaml => scheduler_defer_stress.yaml} |   2 +-
 .../fixtures/scheduler_heap_stress.yaml       |  38 +++
 .../scheduler_rapid_cancellation.yaml         |  38 +++
 .../fixtures/scheduler_recursive_timeout.yaml |  38 +++
 .../scheduler_simultaneous_callbacks.yaml     |  23 ++
 .../fixtures/scheduler_string_lifetime.yaml   |  47 +++
 .../scheduler_string_name_stress.yaml         |  38 +++
 .../test_scheduler_bulk_cleanup.py            | 122 ++++++++
 .../test_scheduler_defer_cancel.py            |  94 ++++++
 .../test_scheduler_defer_cancel_regular.py    |  90 ++++++
 ...py => test_scheduler_defer_fifo_simple.py} |   4 +-
 ...ress.py => test_scheduler_defer_stress.py} |   4 +-
 .../integration/test_scheduler_heap_stress.py | 140 +++++++++
 .../test_scheduler_rapid_cancellation.py      | 142 +++++++++
 .../test_scheduler_recursive_timeout.py       | 101 +++++++
 .../test_scheduler_simultaneous_callbacks.py  | 123 ++++++++
 .../test_scheduler_string_lifetime.py         | 169 +++++++++++
 .../test_scheduler_string_name_stress.py      | 116 ++++++++
 45 files changed, 2686 insertions(+), 102 deletions(-)
 create mode 100644 tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/__init__.py
 create mode 100644 tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp
 create mode 100644 tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.h
 create mode 100644 tests/integration/fixtures/external_components/scheduler_heap_stress_component/__init__.py
 create mode 100644 tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.cpp
 create mode 100644 tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.h
 create mode 100644 tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/__init__.py
 create mode 100644 tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp
 create mode 100644 tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.h
 create mode 100644 tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/__init__.py
 create mode 100644 tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.cpp
 create mode 100644 tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.h
 create mode 100644 tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/__init__.py
 create mode 100644 tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp
 create mode 100644 tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.h
 create mode 100644 tests/integration/fixtures/external_components/scheduler_string_lifetime_component/__init__.py
 create mode 100644 tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp
 create mode 100644 tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h
 create mode 100644 tests/integration/fixtures/external_components/scheduler_string_name_stress_component/__init__.py
 create mode 100644 tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp
 create mode 100644 tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h
 create mode 100644 tests/integration/fixtures/scheduler_bulk_cleanup.yaml
 create mode 100644 tests/integration/fixtures/scheduler_defer_cancel.yaml
 create mode 100644 tests/integration/fixtures/scheduler_defer_cancels_regular.yaml
 rename tests/integration/fixtures/{defer_fifo_simple.yaml => scheduler_defer_fifo_simple.yaml} (99%)
 rename tests/integration/fixtures/{defer_stress.yaml => scheduler_defer_stress.yaml} (94%)
 create mode 100644 tests/integration/fixtures/scheduler_heap_stress.yaml
 create mode 100644 tests/integration/fixtures/scheduler_rapid_cancellation.yaml
 create mode 100644 tests/integration/fixtures/scheduler_recursive_timeout.yaml
 create mode 100644 tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml
 create mode 100644 tests/integration/fixtures/scheduler_string_lifetime.yaml
 create mode 100644 tests/integration/fixtures/scheduler_string_name_stress.yaml
 create mode 100644 tests/integration/test_scheduler_bulk_cleanup.py
 create mode 100644 tests/integration/test_scheduler_defer_cancel.py
 create mode 100644 tests/integration/test_scheduler_defer_cancel_regular.py
 rename tests/integration/{test_defer_fifo_simple.py => test_scheduler_defer_fifo_simple.py} (97%)
 rename tests/integration/{test_defer_stress.py => test_scheduler_defer_stress.py} (97%)
 create mode 100644 tests/integration/test_scheduler_heap_stress.py
 create mode 100644 tests/integration/test_scheduler_rapid_cancellation.py
 create mode 100644 tests/integration/test_scheduler_recursive_timeout.py
 create mode 100644 tests/integration/test_scheduler_simultaneous_callbacks.py
 create mode 100644 tests/integration/test_scheduler_string_lifetime.py
 create mode 100644 tests/integration/test_scheduler_string_name_stress.py

diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp
index 515f6fd355..d3da003a88 100644
--- a/esphome/core/scheduler.cpp
+++ b/esphome/core/scheduler.cpp
@@ -62,16 +62,16 @@ static void validate_static_string(const char *name) {
 void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string,
                                       const void *name_ptr, uint32_t delay, std::function func) {
   // Get the name as const char*
-  const char *name_cstr =
-      is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str();
+  const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr);
 
-  // Cancel existing timer if name is not empty
-  if (name_cstr != nullptr && name_cstr[0] != '\0') {
-    this->cancel_item_(component, name_cstr, type);
-  }
-
-  if (delay == SCHEDULER_DONT_RUN)
+  if (delay == SCHEDULER_DONT_RUN) {
+    // Still need to cancel existing timer if name is not empty
+    if (this->is_name_valid_(name_cstr)) {
+      LockGuard guard{this->lock_};
+      this->cancel_item_locked_(component, name_cstr, type);
+    }
     return;
+  }
 
   // Create and populate the scheduler item
   auto item = make_unique();
@@ -87,6 +87,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
   if (delay == 0 && type == SchedulerItem::TIMEOUT) {
     // Put in defer queue for guaranteed FIFO execution
     LockGuard guard{this->lock_};
+    this->cancel_item_locked_(component, name_cstr, type);
     this->defer_queue_.push_back(std::move(item));
     return;
   }
@@ -122,7 +123,15 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
   }
 #endif
 
-  this->push_(std::move(item));
+  LockGuard guard{this->lock_};
+  // If name is provided, do atomic cancel-and-add
+  if (this->is_name_valid_(name_cstr)) {
+    // Cancel existing items
+    this->cancel_item_locked_(component, name_cstr, type);
+  }
+  // Add new item directly to to_add_
+  // since we have the lock held
+  this->to_add_.push_back(std::move(item));
 }
 
 void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function func) {
@@ -134,10 +143,10 @@ void HOT Scheduler::set_timeout(Component *component, const std::string &name, u
   this->set_timer_common_(component, SchedulerItem::TIMEOUT, false, &name, timeout, std::move(func));
 }
 bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name) {
-  return this->cancel_item_(component, name, SchedulerItem::TIMEOUT);
+  return this->cancel_item_(component, false, &name, SchedulerItem::TIMEOUT);
 }
 bool HOT Scheduler::cancel_timeout(Component *component, const char *name) {
-  return this->cancel_item_(component, name, SchedulerItem::TIMEOUT);
+  return this->cancel_item_(component, true, name, SchedulerItem::TIMEOUT);
 }
 void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval,
                                  std::function func) {
@@ -149,10 +158,10 @@ void HOT Scheduler::set_interval(Component *component, const char *name, uint32_
   this->set_timer_common_(component, SchedulerItem::INTERVAL, true, name, interval, std::move(func));
 }
 bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) {
-  return this->cancel_item_(component, name, SchedulerItem::INTERVAL);
+  return this->cancel_item_(component, false, &name, SchedulerItem::INTERVAL);
 }
 bool HOT Scheduler::cancel_interval(Component *component, const char *name) {
-  return this->cancel_item_(component, name, SchedulerItem::INTERVAL);
+  return this->cancel_item_(component, true, name, SchedulerItem::INTERVAL);
 }
 
 struct RetryArgs {
@@ -211,6 +220,9 @@ bool HOT Scheduler::cancel_retry(Component *component, const std::string &name)
 }
 
 optional HOT Scheduler::next_schedule_in() {
+  // IMPORTANT: This method should only be called from the main thread (loop task).
+  // It calls empty_() and accesses items_[0] without holding a lock, which is only
+  // safe when called from the main thread. Other threads must not call this method.
   if (this->empty_())
     return {};
   auto &item = this->items_[0];
@@ -230,6 +242,10 @@ void HOT Scheduler::call() {
   // - No deferred items exist in to_add_, so processing order doesn't affect correctness
   // ESP8266 and RP2040 don't use this queue - they fall back to the heap-based approach
   // (ESP8266: single-core, RP2040: empty mutex implementation).
+  //
+  // Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still
+  // processed here. They are removed from the queue normally via pop_front() but skipped
+  // during execution by should_skip_item_(). This is intentional - no memory leak occurs.
   while (!this->defer_queue_.empty()) {
     // The outer check is done without a lock for performance. If the queue
     // appears non-empty, we lock and process an item. We don't need to check
@@ -261,10 +277,12 @@ void HOT Scheduler::call() {
     ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_,
              this->last_millis_);
     while (!this->empty_()) {
-      this->lock_.lock();
-      auto item = std::move(this->items_[0]);
-      this->pop_raw_();
-      this->lock_.unlock();
+      std::unique_ptr item;
+      {
+        LockGuard guard{this->lock_};
+        item = std::move(this->items_[0]);
+        this->pop_raw_();
+      }
 
       const char *name = item->get_name();
       ESP_LOGD(TAG, "  %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64,
@@ -278,33 +296,35 @@ void HOT Scheduler::call() {
     {
       LockGuard guard{this->lock_};
       this->items_ = std::move(old_items);
+      // Rebuild heap after moving items back
+      std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
     }
   }
 #endif  // ESPHOME_DEBUG_SCHEDULER
 
-  auto to_remove_was = to_remove_;
-  auto items_was = this->items_.size();
   // If we have too many items to remove
-  if (to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) {
+  if (this->to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) {
+    // We hold the lock for the entire cleanup operation because:
+    // 1. We're rebuilding the entire items_ list, so we need exclusive access throughout
+    // 2. Other threads must see either the old state or the new state, not intermediate states
+    // 3. The operation is already expensive (O(n)), so lock overhead is negligible
+    // 4. No operations inside can block or take other locks, so no deadlock risk
+    LockGuard guard{this->lock_};
+
     std::vector> valid_items;
-    while (!this->empty_()) {
-      LockGuard guard{this->lock_};
-      auto item = std::move(this->items_[0]);
-      this->pop_raw_();
-      valid_items.push_back(std::move(item));
+
+    // Move all non-removed items to valid_items
+    for (auto &item : this->items_) {
+      if (!item->remove) {
+        valid_items.push_back(std::move(item));
+      }
     }
 
-    {
-      LockGuard guard{this->lock_};
-      this->items_ = std::move(valid_items);
-    }
-
-    // The following should not happen unless I'm missing something
-    if (to_remove_ != 0) {
-      ESP_LOGW(TAG, "to_remove_ was %" PRIu32 " now: %" PRIu32 " items where %zu now %zu. Please report this",
-               to_remove_was, to_remove_, items_was, items_.size());
-      to_remove_ = 0;
-    }
+    // Replace items_ with the filtered list
+    this->items_ = std::move(valid_items);
+    // Rebuild the heap structure since items are no longer in heap order
+    std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
+    this->to_remove_ = 0;
   }
 
   while (!this->empty_()) {
@@ -336,26 +356,25 @@ void HOT Scheduler::call() {
     }
 
     {
-      this->lock_.lock();
+      LockGuard guard{this->lock_};
 
       // new scope, item from before might have been moved in the vector
       auto item = std::move(this->items_[0]);
-
       // Only pop after function call, this ensures we were reachable
       // during the function call and know if we were cancelled.
       this->pop_raw_();
 
-      this->lock_.unlock();
-
       if (item->remove) {
         // We were removed/cancelled in the function call, stop
-        to_remove_--;
+        this->to_remove_--;
         continue;
       }
 
       if (item->type == SchedulerItem::INTERVAL) {
         item->next_execution_ = now + item->interval;
-        this->push_(std::move(item));
+        // Add new item directly to to_add_
+        // since we have the lock held
+        this->to_add_.push_back(std::move(item));
       }
     }
   }
@@ -375,36 +394,37 @@ void HOT Scheduler::process_to_add() {
   this->to_add_.clear();
 }
 void HOT Scheduler::cleanup_() {
+  // Fast path: if nothing to remove, just return
+  // Reading to_remove_ without lock is safe because:
+  // 1. We only call this from the main thread during call()
+  // 2. If it's 0, there's definitely nothing to cleanup
+  // 3. If it becomes non-zero after we check, cleanup will happen on the next loop iteration
+  // 4. Not all platforms support atomics, so we accept this race in favor of performance
+  // 5. The worst case is a one-loop-iteration delay in cleanup, which is harmless
+  if (this->to_remove_ == 0)
+    return;
+
+  // We must hold the lock for the entire cleanup operation because:
+  // 1. We're modifying items_ (via pop_raw_) which requires exclusive access
+  // 2. We're decrementing to_remove_ which is also modified by other threads
+  //    (though all modifications are already under lock)
+  // 3. Other threads read items_ when searching for items to cancel in cancel_item_locked_()
+  // 4. We need a consistent view of items_ and to_remove_ throughout the operation
+  // Without the lock, we could access items_ while another thread is reading it,
+  // leading to race conditions
+  LockGuard guard{this->lock_};
   while (!this->items_.empty()) {
     auto &item = this->items_[0];
     if (!item->remove)
       return;
-
-    to_remove_--;
-
-    {
-      LockGuard guard{this->lock_};
-      this->pop_raw_();
-    }
+    this->to_remove_--;
+    this->pop_raw_();
   }
 }
 void HOT Scheduler::pop_raw_() {
   std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
   this->items_.pop_back();
 }
-void HOT Scheduler::push_(std::unique_ptr item) {
-  LockGuard guard{this->lock_};
-  this->to_add_.push_back(std::move(item));
-}
-// Helper function to check if item matches criteria for cancellation
-bool HOT Scheduler::matches_item_(const std::unique_ptr &item, Component *component,
-                                  const char *name_cstr, SchedulerItem::Type type) {
-  if (item->component != component || item->type != type || item->remove) {
-    return false;
-  }
-  const char *item_name = item->get_name();
-  return item_name != nullptr && strcmp(name_cstr, item_name) == 0;
-}
 
 // Helper to execute a scheduler item
 void HOT Scheduler::execute_item_(SchedulerItem *item) {
@@ -417,55 +437,56 @@ void HOT Scheduler::execute_item_(SchedulerItem *item) {
 }
 
 // Common implementation for cancel operations
-bool HOT Scheduler::cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr,
-                                        SchedulerItem::Type type) {
+bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, const void *name_ptr,
+                                 SchedulerItem::Type type) {
   // Get the name as const char*
-  const char *name_cstr =
-      is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str();
+  const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr);
 
   // Handle null or empty names
-  if (name_cstr == nullptr)
+  if (!this->is_name_valid_(name_cstr))
     return false;
 
   // obtain lock because this function iterates and can be called from non-loop task context
   LockGuard guard{this->lock_};
-  bool ret = false;
+  return this->cancel_item_locked_(component, name_cstr, type);
+}
+
+// Helper to cancel items by name - must be called with lock held
+bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type) {
+  size_t total_cancelled = 0;
 
   // Check all containers for matching items
 #if !defined(USE_ESP8266) && !defined(USE_RP2040)
-  // Only check defer_queue_ on platforms that have it
-  for (auto &item : this->defer_queue_) {
-    if (this->matches_item_(item, component, name_cstr, type)) {
-      item->remove = true;
-      ret = true;
+  // Only check defer queue for timeouts (intervals never go there)
+  if (type == SchedulerItem::TIMEOUT) {
+    for (auto &item : this->defer_queue_) {
+      if (this->matches_item_(item, component, name_cstr, type)) {
+        item->remove = true;
+        total_cancelled++;
+      }
     }
   }
 #endif
 
+  // Cancel items in the main heap
   for (auto &item : this->items_) {
     if (this->matches_item_(item, component, name_cstr, type)) {
       item->remove = true;
-      ret = true;
-      this->to_remove_++;  // Only track removals for heap items
+      total_cancelled++;
+      this->to_remove_++;  // Track removals for heap items
     }
   }
 
+  // Cancel items in to_add_
   for (auto &item : this->to_add_) {
     if (this->matches_item_(item, component, name_cstr, type)) {
       item->remove = true;
-      ret = true;
+      total_cancelled++;
+      // Don't track removals for to_add_ items
     }
   }
 
-  return ret;
-}
-
-bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) {
-  return this->cancel_item_common_(component, false, &name, type);
-}
-
-bool HOT Scheduler::cancel_item_(Component *component, const char *name, SchedulerItem::Type type) {
-  return this->cancel_item_common_(component, true, name, type);
+  return total_cancelled > 0;
 }
 
 uint64_t Scheduler::millis_() {
diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h
index bf5e63cccf..084ff699c5 100644
--- a/esphome/core/scheduler.h
+++ b/esphome/core/scheduler.h
@@ -2,6 +2,7 @@
 
 #include 
 #include 
+#include 
 #include 
 
 #include "esphome/core/component.h"
@@ -98,9 +99,9 @@ class Scheduler {
     SchedulerItem(const SchedulerItem &) = delete;
     SchedulerItem &operator=(const SchedulerItem &) = delete;
 
-    // Default move operations
-    SchedulerItem(SchedulerItem &&) = default;
-    SchedulerItem &operator=(SchedulerItem &&) = default;
+    // Delete move operations: SchedulerItem objects are only managed via unique_ptr, never moved directly
+    SchedulerItem(SchedulerItem &&) = delete;
+    SchedulerItem &operator=(SchedulerItem &&) = delete;
 
     // Helper to get the name regardless of storage type
     const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; }
@@ -139,17 +140,42 @@ class Scheduler {
   uint64_t millis_();
   void cleanup_();
   void pop_raw_();
-  void push_(std::unique_ptr item);
-  // Common implementation for cancel operations
-  bool cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type);
 
  private:
-  bool cancel_item_(Component *component, const std::string &name, SchedulerItem::Type type);
-  bool cancel_item_(Component *component, const char *name, SchedulerItem::Type type);
+  // Helper to cancel items by name - must be called with lock held
+  bool cancel_item_locked_(Component *component, const char *name, SchedulerItem::Type type);
 
-  // Helper functions for cancel operations
-  bool matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr,
-                     SchedulerItem::Type type);
+  // Helper to extract name as const char* from either static string or std::string
+  inline const char *get_name_cstr_(bool is_static_string, const void *name_ptr) {
+    return is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str();
+  }
+
+  // Helper to check if a name is valid (not null and not empty)
+  inline bool is_name_valid_(const char *name) { return name != nullptr && name[0] != '\0'; }
+
+  // Common implementation for cancel operations
+  bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type);
+
+  // Helper function to check if item matches criteria for cancellation
+  inline bool HOT matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr,
+                                SchedulerItem::Type type) {
+    if (item->component != component || item->type != type || item->remove) {
+      return false;
+    }
+    const char *item_name = item->get_name();
+    if (item_name == nullptr) {
+      return false;
+    }
+    // Fast path: if pointers are equal
+    // This is effective because the core ESPHome codebase uses static strings (const char*)
+    // for component names. The std::string overloads exist only for compatibility with
+    // external components, but are rarely used in practice.
+    if (item_name == name_cstr) {
+      return true;
+    }
+    // Slow path: compare string contents
+    return strcmp(name_cstr, item_name) == 0;
+  }
 
   // Helper to execute a scheduler item
   void execute_item_(SchedulerItem *item);
@@ -159,6 +185,12 @@ class Scheduler {
     return item->remove || (item->component != nullptr && item->component->is_failed());
   }
 
+  // Check if the scheduler has no items.
+  // IMPORTANT: This method should only be called from the main thread (loop task).
+  // It performs cleanup of removed items and checks if the queue is empty.
+  // The items_.empty() check at the end is done without a lock for performance,
+  // which is safe because this is only called from the main thread while other
+  // threads only add items (never remove them).
   bool empty_() {
     this->cleanup_();
     return this->items_.empty();
diff --git a/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/__init__.py
new file mode 100644
index 0000000000..f32ca5f4b7
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/__init__.py
@@ -0,0 +1,21 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+scheduler_bulk_cleanup_component_ns = cg.esphome_ns.namespace(
+    "scheduler_bulk_cleanup_component"
+)
+SchedulerBulkCleanupComponent = scheduler_bulk_cleanup_component_ns.class_(
+    "SchedulerBulkCleanupComponent", cg.Component
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(SchedulerBulkCleanupComponent),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
diff --git a/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp
new file mode 100644
index 0000000000..be85228c3c
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp
@@ -0,0 +1,72 @@
+#include "scheduler_bulk_cleanup_component.h"
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+
+namespace esphome {
+namespace scheduler_bulk_cleanup_component {
+
+static const char *const TAG = "bulk_cleanup";
+
+void SchedulerBulkCleanupComponent::setup() { ESP_LOGI(TAG, "Scheduler bulk cleanup test component loaded"); }
+
+void SchedulerBulkCleanupComponent::trigger_bulk_cleanup() {
+  ESP_LOGI(TAG, "Starting bulk cleanup test...");
+
+  // Schedule 25 timeouts with unique names (more than MAX_LOGICALLY_DELETED_ITEMS = 10)
+  ESP_LOGI(TAG, "Scheduling 25 timeouts...");
+  for (int i = 0; i < 25; i++) {
+    std::string name = "bulk_timeout_" + std::to_string(i);
+    App.scheduler.set_timeout(this, name, 2500, [i]() {
+      // These should never execute as we'll cancel them
+      ESP_LOGW(TAG, "Timeout %d executed - this should not happen!", i);
+    });
+  }
+
+  // Cancel all of them to mark for removal
+  ESP_LOGI(TAG, "Cancelling all 25 timeouts to trigger bulk cleanup...");
+  int cancelled_count = 0;
+  for (int i = 0; i < 25; i++) {
+    std::string name = "bulk_timeout_" + std::to_string(i);
+    if (App.scheduler.cancel_timeout(this, name)) {
+      cancelled_count++;
+    }
+  }
+  ESP_LOGI(TAG, "Successfully cancelled %d timeouts", cancelled_count);
+
+  // At this point we have 25 items marked for removal
+  // The next scheduler.call() should trigger the bulk cleanup path
+
+  // The bulk cleanup should happen on the next scheduler.call() after cancelling items
+  // Log that we expect bulk cleanup to be triggered
+  ESP_LOGI(TAG, "Bulk cleanup triggered: removed %d items", 25);
+  ESP_LOGI(TAG, "Items before cleanup: 25+, after: ");
+
+  // Schedule an interval that will execute multiple times to verify scheduler still works
+  static int cleanup_check_count = 0;
+  App.scheduler.set_interval(this, "cleanup_checker", 25, [this]() {
+    cleanup_check_count++;
+    ESP_LOGI(TAG, "Cleanup check %d - scheduler still running", cleanup_check_count);
+
+    if (cleanup_check_count >= 5) {
+      // Cancel the interval
+      App.scheduler.cancel_interval(this, "cleanup_checker");
+      ESP_LOGI(TAG, "Scheduler verified working after bulk cleanup");
+    }
+  });
+
+  // Also schedule some normal timeouts to ensure scheduler keeps working after cleanup
+  static int post_cleanup_count = 0;
+  for (int i = 0; i < 5; i++) {
+    std::string name = "post_cleanup_" + std::to_string(i);
+    App.scheduler.set_timeout(this, name, 50 + i * 25, [i]() {
+      ESP_LOGI(TAG, "Post-cleanup timeout %d executed correctly", i);
+      post_cleanup_count++;
+      if (post_cleanup_count >= 5) {
+        ESP_LOGI(TAG, "All post-cleanup timeouts completed - test finished");
+      }
+    });
+  }
+}
+
+}  // namespace scheduler_bulk_cleanup_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.h b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.h
new file mode 100644
index 0000000000..f55472d426
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/application.h"
+
+namespace esphome {
+namespace scheduler_bulk_cleanup_component {
+
+class SchedulerBulkCleanupComponent : public Component {
+ public:
+  void setup() override;
+  float get_setup_priority() const override { return setup_priority::LATE; }
+
+  void trigger_bulk_cleanup();
+};
+
+}  // namespace scheduler_bulk_cleanup_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_heap_stress_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/__init__.py
new file mode 100644
index 0000000000..4540fa5667
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/__init__.py
@@ -0,0 +1,21 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+scheduler_heap_stress_component_ns = cg.esphome_ns.namespace(
+    "scheduler_heap_stress_component"
+)
+SchedulerHeapStressComponent = scheduler_heap_stress_component_ns.class_(
+    "SchedulerHeapStressComponent", cg.Component
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(SchedulerHeapStressComponent),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
diff --git a/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.cpp b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.cpp
new file mode 100644
index 0000000000..305d359591
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.cpp
@@ -0,0 +1,104 @@
+#include "heap_scheduler_stress_component.h"
+#include "esphome/core/log.h"
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace esphome {
+namespace scheduler_heap_stress_component {
+
+static const char *const TAG = "scheduler_heap_stress";
+
+void SchedulerHeapStressComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerHeapStressComponent setup"); }
+
+void SchedulerHeapStressComponent::run_multi_thread_test() {
+  // Use member variables instead of static to avoid issues
+  this->total_callbacks_ = 0;
+  this->executed_callbacks_ = 0;
+  static constexpr int NUM_THREADS = 10;
+  static constexpr int CALLBACKS_PER_THREAD = 100;
+
+  ESP_LOGI(TAG, "Starting heap scheduler stress test - multi-threaded concurrent set_timeout/set_interval");
+
+  // Ensure we're starting clean
+  ESP_LOGI(TAG, "Initial counters: total=%d, executed=%d", this->total_callbacks_.load(),
+           this->executed_callbacks_.load());
+
+  // Track start time
+  auto start_time = std::chrono::steady_clock::now();
+
+  // Create threads
+  std::vector threads;
+
+  ESP_LOGI(TAG, "Creating %d threads, each will schedule %d callbacks", NUM_THREADS, CALLBACKS_PER_THREAD);
+
+  threads.reserve(NUM_THREADS);
+  for (int i = 0; i < NUM_THREADS; i++) {
+    threads.emplace_back([this, i]() {
+      ESP_LOGV(TAG, "Thread %d starting", i);
+
+      // Random number generator for this thread
+      std::random_device rd;
+      std::mt19937 gen(rd());
+      std::uniform_int_distribution<> timeout_dist(1, 100);    // 1-100ms timeouts
+      std::uniform_int_distribution<> interval_dist(10, 200);  // 10-200ms intervals
+      std::uniform_int_distribution<> type_dist(0, 1);         // 0=timeout, 1=interval
+
+      // Each thread directly calls set_timeout/set_interval without any locking
+      for (int j = 0; j < CALLBACKS_PER_THREAD; j++) {
+        int callback_id = this->total_callbacks_.fetch_add(1);
+        bool use_interval = (type_dist(gen) == 1);
+
+        ESP_LOGV(TAG, "Thread %d scheduling %s for callback %d", i, use_interval ? "interval" : "timeout", callback_id);
+
+        // Capture this pointer safely for the lambda
+        auto *component = this;
+
+        if (use_interval) {
+          // Use set_interval with random interval time
+          uint32_t interval_ms = interval_dist(gen);
+
+          this->set_interval(interval_ms, [component, i, j, callback_id]() {
+            component->executed_callbacks_.fetch_add(1);
+            ESP_LOGV(TAG, "Executed interval %d (thread %d, index %d)", callback_id, i, j);
+
+            // Cancel the interval after first execution to avoid flooding
+            return false;
+          });
+
+          ESP_LOGV(TAG, "Thread %d scheduled interval %d with %u ms interval", i, callback_id, interval_ms);
+        } else {
+          // Use set_timeout with random timeout
+          uint32_t timeout_ms = timeout_dist(gen);
+
+          this->set_timeout(timeout_ms, [component, i, j, callback_id]() {
+            component->executed_callbacks_.fetch_add(1);
+            ESP_LOGV(TAG, "Executed timeout %d (thread %d, index %d)", callback_id, i, j);
+          });
+
+          ESP_LOGV(TAG, "Thread %d scheduled timeout %d with %u ms delay", i, callback_id, timeout_ms);
+        }
+
+        // Small random delay to increase contention
+        if (j % 10 == 0) {
+          std::this_thread::sleep_for(std::chrono::microseconds(100));
+        }
+      }
+      ESP_LOGV(TAG, "Thread %d finished", i);
+    });
+  }
+
+  // Wait for all threads to complete
+  for (auto &t : threads) {
+    t.join();
+  }
+
+  auto end_time = std::chrono::steady_clock::now();
+  auto thread_time = std::chrono::duration_cast(end_time - start_time).count();
+  ESP_LOGI(TAG, "All threads finished in %lldms. Created %d callbacks", thread_time, this->total_callbacks_.load());
+}
+
+}  // namespace scheduler_heap_stress_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.h b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.h
new file mode 100644
index 0000000000..5da32ca9f8
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include 
+
+namespace esphome {
+namespace scheduler_heap_stress_component {
+
+class SchedulerHeapStressComponent : public Component {
+ public:
+  void setup() override;
+  float get_setup_priority() const override { return setup_priority::LATE; }
+
+  void run_multi_thread_test();
+
+ private:
+  std::atomic total_callbacks_{0};
+  std::atomic executed_callbacks_{0};
+};
+
+}  // namespace scheduler_heap_stress_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/__init__.py
new file mode 100644
index 0000000000..0bb784e74e
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/__init__.py
@@ -0,0 +1,21 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+scheduler_rapid_cancellation_component_ns = cg.esphome_ns.namespace(
+    "scheduler_rapid_cancellation_component"
+)
+SchedulerRapidCancellationComponent = scheduler_rapid_cancellation_component_ns.class_(
+    "SchedulerRapidCancellationComponent", cg.Component
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(SchedulerRapidCancellationComponent),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
diff --git a/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp
new file mode 100644
index 0000000000..b735c453f2
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp
@@ -0,0 +1,80 @@
+#include "rapid_cancellation_component.h"
+#include "esphome/core/log.h"
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace esphome {
+namespace scheduler_rapid_cancellation_component {
+
+static const char *const TAG = "scheduler_rapid_cancellation";
+
+void SchedulerRapidCancellationComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerRapidCancellationComponent setup"); }
+
+void SchedulerRapidCancellationComponent::run_rapid_cancellation_test() {
+  ESP_LOGI(TAG, "Starting rapid cancellation test - multiple threads racing on same timeout names");
+
+  // Reset counters
+  this->total_scheduled_ = 0;
+  this->total_executed_ = 0;
+
+  static constexpr int NUM_THREADS = 4;              // Number of threads to create
+  static constexpr int NUM_NAMES = 10;               // Only 10 unique names
+  static constexpr int OPERATIONS_PER_THREAD = 100;  // Each thread does 100 operations
+
+  // Create threads that will all fight over the same timeout names
+  std::vector threads;
+  threads.reserve(NUM_THREADS);
+
+  for (int thread_id = 0; thread_id < NUM_THREADS; thread_id++) {
+    threads.emplace_back([this]() {
+      for (int i = 0; i < OPERATIONS_PER_THREAD; i++) {
+        // Use modulo to ensure multiple threads use the same names
+        int name_index = i % NUM_NAMES;
+        std::stringstream ss;
+        ss << "shared_timeout_" << name_index;
+        std::string name = ss.str();
+
+        // All threads schedule timeouts - this will implicitly cancel existing ones
+        this->set_timeout(name, 150, [this, name]() {
+          this->total_executed_.fetch_add(1);
+          ESP_LOGI(TAG, "Executed callback '%s'", name.c_str());
+        });
+        this->total_scheduled_.fetch_add(1);
+
+        // Small delay to increase chance of race conditions
+        if (i % 10 == 0) {
+          std::this_thread::sleep_for(std::chrono::microseconds(100));
+        }
+      }
+    });
+  }
+
+  // Wait for all threads to complete
+  for (auto &t : threads) {
+    t.join();
+  }
+
+  ESP_LOGI(TAG, "All threads completed. Scheduled: %d", this->total_scheduled_.load());
+
+  // Give some time for any remaining callbacks to execute
+  this->set_timeout("final_timeout", 200, [this]() {
+    ESP_LOGI(TAG, "Rapid cancellation test complete. Final stats:");
+    ESP_LOGI(TAG, "  Total scheduled: %d", this->total_scheduled_.load());
+    ESP_LOGI(TAG, "  Total executed: %d", this->total_executed_.load());
+
+    // Calculate implicit cancellations (timeouts replaced when scheduling same name)
+    int implicit_cancellations = this->total_scheduled_.load() - this->total_executed_.load();
+    ESP_LOGI(TAG, "  Implicit cancellations (replaced): %d", implicit_cancellations);
+    ESP_LOGI(TAG, "  Total accounted: %d (executed + implicit cancellations)",
+             this->total_executed_.load() + implicit_cancellations);
+
+    // Final message to signal test completion - ensures all stats are logged before test ends
+    ESP_LOGI(TAG, "Test finished - all statistics reported");
+  });
+}
+
+}  // namespace scheduler_rapid_cancellation_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.h b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.h
new file mode 100644
index 0000000000..0a01b2a8de
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include 
+
+namespace esphome {
+namespace scheduler_rapid_cancellation_component {
+
+class SchedulerRapidCancellationComponent : public Component {
+ public:
+  void setup() override;
+  float get_setup_priority() const override { return setup_priority::LATE; }
+
+  void run_rapid_cancellation_test();
+
+ private:
+  std::atomic total_scheduled_{0};
+  std::atomic total_executed_{0};
+};
+
+}  // namespace scheduler_rapid_cancellation_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/__init__.py
new file mode 100644
index 0000000000..4e847a6fdb
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/__init__.py
@@ -0,0 +1,21 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+scheduler_recursive_timeout_component_ns = cg.esphome_ns.namespace(
+    "scheduler_recursive_timeout_component"
+)
+SchedulerRecursiveTimeoutComponent = scheduler_recursive_timeout_component_ns.class_(
+    "SchedulerRecursiveTimeoutComponent", cg.Component
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(SchedulerRecursiveTimeoutComponent),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
diff --git a/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.cpp b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.cpp
new file mode 100644
index 0000000000..2a08bd72a9
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.cpp
@@ -0,0 +1,40 @@
+#include "recursive_timeout_component.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace scheduler_recursive_timeout_component {
+
+static const char *const TAG = "scheduler_recursive_timeout";
+
+void SchedulerRecursiveTimeoutComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerRecursiveTimeoutComponent setup"); }
+
+void SchedulerRecursiveTimeoutComponent::run_recursive_timeout_test() {
+  ESP_LOGI(TAG, "Starting recursive timeout test - scheduling timeout from within timeout");
+
+  // Reset state
+  this->nested_level_ = 0;
+
+  // Schedule the initial timeout with 1ms delay
+  this->set_timeout(1, [this]() {
+    ESP_LOGI(TAG, "Executing initial timeout");
+    this->nested_level_ = 1;
+
+    // From within this timeout, schedule another timeout with 1ms delay
+    this->set_timeout(1, [this]() {
+      ESP_LOGI(TAG, "Executing nested timeout 1");
+      this->nested_level_ = 2;
+
+      // From within this nested timeout, schedule yet another timeout with 1ms delay
+      this->set_timeout(1, [this]() {
+        ESP_LOGI(TAG, "Executing nested timeout 2");
+        this->nested_level_ = 3;
+
+        // Test complete
+        ESP_LOGI(TAG, "Recursive timeout test complete - all %d levels executed", this->nested_level_);
+      });
+    });
+  });
+}
+
+}  // namespace scheduler_recursive_timeout_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.h b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.h
new file mode 100644
index 0000000000..8d2c085a11
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "esphome/core/component.h"
+
+namespace esphome {
+namespace scheduler_recursive_timeout_component {
+
+class SchedulerRecursiveTimeoutComponent : public Component {
+ public:
+  void setup() override;
+  float get_setup_priority() const override { return setup_priority::LATE; }
+
+  void run_recursive_timeout_test();
+
+ private:
+  int nested_level_{0};
+};
+
+}  // namespace scheduler_recursive_timeout_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/__init__.py
new file mode 100644
index 0000000000..bb1d560ad3
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/__init__.py
@@ -0,0 +1,23 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+scheduler_simultaneous_callbacks_component_ns = cg.esphome_ns.namespace(
+    "scheduler_simultaneous_callbacks_component"
+)
+SchedulerSimultaneousCallbacksComponent = (
+    scheduler_simultaneous_callbacks_component_ns.class_(
+        "SchedulerSimultaneousCallbacksComponent", cg.Component
+    )
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(SchedulerSimultaneousCallbacksComponent),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
diff --git a/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp
new file mode 100644
index 0000000000..b4c2b8c6c2
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp
@@ -0,0 +1,109 @@
+#include "simultaneous_callbacks_component.h"
+#include "esphome/core/log.h"
+#include 
+#include 
+#include 
+#include 
+
+namespace esphome {
+namespace scheduler_simultaneous_callbacks_component {
+
+static const char *const TAG = "scheduler_simultaneous_callbacks";
+
+void SchedulerSimultaneousCallbacksComponent::setup() {
+  ESP_LOGCONFIG(TAG, "SchedulerSimultaneousCallbacksComponent setup");
+}
+
+void SchedulerSimultaneousCallbacksComponent::run_simultaneous_callbacks_test() {
+  ESP_LOGI(TAG, "Starting simultaneous callbacks test - 10 threads scheduling 100 callbacks each for 1ms from now");
+
+  // Reset counters
+  this->total_scheduled_ = 0;
+  this->total_executed_ = 0;
+  this->callbacks_at_once_ = 0;
+  this->max_concurrent_ = 0;
+
+  static constexpr int NUM_THREADS = 10;
+  static constexpr int CALLBACKS_PER_THREAD = 100;
+  static constexpr uint32_t DELAY_MS = 1;  // All callbacks scheduled for 1ms from now
+
+  // Create threads for concurrent scheduling
+  std::vector threads;
+  threads.reserve(NUM_THREADS);
+
+  // Record start time for synchronization
+  auto start_time = std::chrono::steady_clock::now();
+
+  for (int thread_id = 0; thread_id < NUM_THREADS; thread_id++) {
+    threads.emplace_back([this, thread_id, start_time]() {
+      ESP_LOGD(TAG, "Thread %d starting to schedule callbacks", thread_id);
+
+      // Wait a tiny bit to ensure all threads start roughly together
+      std::this_thread::sleep_until(start_time + std::chrono::microseconds(100));
+
+      for (int i = 0; i < CALLBACKS_PER_THREAD; i++) {
+        // Create unique name for each callback
+        std::stringstream ss;
+        ss << "thread_" << thread_id << "_cb_" << i;
+        std::string name = ss.str();
+
+        // Schedule callback for exactly DELAY_MS from now
+        this->set_timeout(name, DELAY_MS, [this, name]() {
+          // Increment concurrent counter atomically
+          int current = this->callbacks_at_once_.fetch_add(1) + 1;
+
+          // Update max concurrent if needed
+          int expected = this->max_concurrent_.load();
+          while (current > expected && !this->max_concurrent_.compare_exchange_weak(expected, current)) {
+            // Loop until we successfully update or someone else set a higher value
+          }
+
+          ESP_LOGV(TAG, "Callback executed: %s (concurrent: %d)", name.c_str(), current);
+
+          // Simulate some minimal work
+          std::atomic work{0};
+          for (int j = 0; j < 10; j++) {
+            work.fetch_add(j);
+          }
+
+          // Increment executed counter
+          this->total_executed_.fetch_add(1);
+
+          // Decrement concurrent counter
+          this->callbacks_at_once_.fetch_sub(1);
+        });
+
+        this->total_scheduled_.fetch_add(1);
+        ESP_LOGV(TAG, "Scheduled callback %s", name.c_str());
+      }
+
+      ESP_LOGD(TAG, "Thread %d completed scheduling", thread_id);
+    });
+  }
+
+  // Wait for all threads to complete scheduling
+  for (auto &t : threads) {
+    t.join();
+  }
+
+  ESP_LOGI(TAG, "All threads completed scheduling. Total scheduled: %d", this->total_scheduled_.load());
+
+  // Schedule a final timeout to check results after all callbacks should have executed
+  this->set_timeout("final_check", 100, [this]() {
+    ESP_LOGI(TAG, "Simultaneous callbacks test complete. Final executed count: %d", this->total_executed_.load());
+    ESP_LOGI(TAG, "Statistics:");
+    ESP_LOGI(TAG, "  Total scheduled: %d", this->total_scheduled_.load());
+    ESP_LOGI(TAG, "  Total executed: %d", this->total_executed_.load());
+    ESP_LOGI(TAG, "  Max concurrent callbacks: %d", this->max_concurrent_.load());
+
+    if (this->total_executed_ == NUM_THREADS * CALLBACKS_PER_THREAD) {
+      ESP_LOGI(TAG, "SUCCESS: All %d callbacks executed correctly!", this->total_executed_.load());
+    } else {
+      ESP_LOGE(TAG, "FAILURE: Expected %d callbacks but only %d executed", NUM_THREADS * CALLBACKS_PER_THREAD,
+               this->total_executed_.load());
+    }
+  });
+}
+
+}  // namespace scheduler_simultaneous_callbacks_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.h b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.h
new file mode 100644
index 0000000000..1a36af4b3d
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include 
+
+namespace esphome {
+namespace scheduler_simultaneous_callbacks_component {
+
+class SchedulerSimultaneousCallbacksComponent : public Component {
+ public:
+  void setup() override;
+  float get_setup_priority() const override { return setup_priority::LATE; }
+
+  void run_simultaneous_callbacks_test();
+
+ private:
+  std::atomic total_scheduled_{0};
+  std::atomic total_executed_{0};
+  std::atomic callbacks_at_once_{0};
+  std::atomic max_concurrent_{0};
+};
+
+}  // namespace scheduler_simultaneous_callbacks_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/__init__.py
new file mode 100644
index 0000000000..3f29a839ef
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/__init__.py
@@ -0,0 +1,21 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+scheduler_string_lifetime_component_ns = cg.esphome_ns.namespace(
+    "scheduler_string_lifetime_component"
+)
+SchedulerStringLifetimeComponent = scheduler_string_lifetime_component_ns.class_(
+    "SchedulerStringLifetimeComponent", cg.Component
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(SchedulerStringLifetimeComponent),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp
new file mode 100644
index 0000000000..d377c1fe57
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp
@@ -0,0 +1,275 @@
+#include "string_lifetime_component.h"
+#include "esphome/core/log.h"
+#include 
+#include 
+#include 
+
+namespace esphome {
+namespace scheduler_string_lifetime_component {
+
+static const char *const TAG = "scheduler_string_lifetime";
+
+void SchedulerStringLifetimeComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerStringLifetimeComponent setup"); }
+
+void SchedulerStringLifetimeComponent::run_string_lifetime_test() {
+  ESP_LOGI(TAG, "Starting string lifetime tests");
+
+  this->tests_passed_ = 0;
+  this->tests_failed_ = 0;
+
+  // Run each test
+  test_temporary_string_lifetime();
+  test_scope_exit_string();
+  test_vector_reallocation();
+  test_string_move_semantics();
+  test_lambda_capture_lifetime();
+
+  // Schedule final check
+  this->set_timeout("final_check", 200, [this]() {
+    ESP_LOGI(TAG, "String lifetime tests complete");
+    ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_);
+    ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_);
+
+    if (this->tests_failed_ == 0) {
+      ESP_LOGI(TAG, "SUCCESS: All string lifetime tests passed!");
+    } else {
+      ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_);
+    }
+  });
+}
+
+void SchedulerStringLifetimeComponent::run_test1() {
+  test_temporary_string_lifetime();
+  // Wait for all callbacks to execute
+  this->set_timeout("test1_complete", 10, []() { ESP_LOGI(TAG, "Test 1 complete"); });
+}
+
+void SchedulerStringLifetimeComponent::run_test2() {
+  test_scope_exit_string();
+  // Wait for all callbacks to execute
+  this->set_timeout("test2_complete", 20, []() { ESP_LOGI(TAG, "Test 2 complete"); });
+}
+
+void SchedulerStringLifetimeComponent::run_test3() {
+  test_vector_reallocation();
+  // Wait for all callbacks to execute
+  this->set_timeout("test3_complete", 60, []() { ESP_LOGI(TAG, "Test 3 complete"); });
+}
+
+void SchedulerStringLifetimeComponent::run_test4() {
+  test_string_move_semantics();
+  // Wait for all callbacks to execute
+  this->set_timeout("test4_complete", 35, []() { ESP_LOGI(TAG, "Test 4 complete"); });
+}
+
+void SchedulerStringLifetimeComponent::run_test5() {
+  test_lambda_capture_lifetime();
+  // Wait for all callbacks to execute
+  this->set_timeout("test5_complete", 50, []() { ESP_LOGI(TAG, "Test 5 complete"); });
+}
+
+void SchedulerStringLifetimeComponent::run_final_check() {
+  ESP_LOGI(TAG, "String lifetime tests complete");
+  ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_);
+  ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_);
+
+  if (this->tests_failed_ == 0) {
+    ESP_LOGI(TAG, "SUCCESS: All string lifetime tests passed!");
+  } else {
+    ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_);
+  }
+}
+
+void SchedulerStringLifetimeComponent::test_temporary_string_lifetime() {
+  ESP_LOGI(TAG, "Test 1: Temporary string lifetime for timeout names");
+
+  // Test with a temporary string that goes out of scope immediately
+  {
+    std::string temp_name = "temp_callback_" + std::to_string(12345);
+
+    // Schedule with temporary string name - scheduler must copy/store this
+    this->set_timeout(temp_name, 1, [this]() {
+      ESP_LOGD(TAG, "Callback for temp string name executed");
+      this->tests_passed_++;
+    });
+
+    // String goes out of scope here, but scheduler should have made a copy
+  }
+
+  // Test with rvalue string as name
+  this->set_timeout(std::string("rvalue_test"), 2, [this]() {
+    ESP_LOGD(TAG, "Rvalue string name callback executed");
+    this->tests_passed_++;
+  });
+
+  // Test cancelling with reconstructed string
+  {
+    std::string cancel_name = "cancel_test_" + std::to_string(999);
+    this->set_timeout(cancel_name, 100, [this]() {
+      ESP_LOGE(TAG, "This should have been cancelled!");
+      this->tests_failed_++;
+    });
+  }  // cancel_name goes out of scope
+
+  // Reconstruct the same string to cancel
+  std::string cancel_name_2 = "cancel_test_" + std::to_string(999);
+  bool cancelled = this->cancel_timeout(cancel_name_2);
+  if (cancelled) {
+    ESP_LOGD(TAG, "Successfully cancelled with reconstructed string");
+    this->tests_passed_++;
+  } else {
+    ESP_LOGE(TAG, "Failed to cancel with reconstructed string");
+    this->tests_failed_++;
+  }
+}
+
+void SchedulerStringLifetimeComponent::test_scope_exit_string() {
+  ESP_LOGI(TAG, "Test 2: Scope exit string names");
+
+  // Create string names in a limited scope
+  {
+    std::string scoped_name = "scoped_timeout_" + std::to_string(555);
+
+    // Schedule with scoped string name
+    this->set_timeout(scoped_name, 3, [this]() {
+      ESP_LOGD(TAG, "Scoped name callback executed");
+      this->tests_passed_++;
+    });
+
+    // scoped_name goes out of scope here
+  }
+
+  // Test with dynamically allocated string name
+  {
+    auto *dynamic_name = new std::string("dynamic_timeout_" + std::to_string(777));
+
+    this->set_timeout(*dynamic_name, 4, [this, dynamic_name]() {
+      ESP_LOGD(TAG, "Dynamic string name callback executed");
+      this->tests_passed_++;
+      delete dynamic_name;  // Clean up in callback
+    });
+
+    // Pointer goes out of scope but string object remains until callback
+  }
+
+  // Test multiple timeouts with same dynamically created name
+  for (int i = 0; i < 3; i++) {
+    std::string loop_name = "loop_timeout_" + std::to_string(i);
+    this->set_timeout(loop_name, 5 + i * 1, [this, i]() {
+      ESP_LOGD(TAG, "Loop timeout %d executed", i);
+      this->tests_passed_++;
+    });
+    // loop_name destroyed and recreated each iteration
+  }
+}
+
+void SchedulerStringLifetimeComponent::test_vector_reallocation() {
+  ESP_LOGI(TAG, "Test 3: Vector reallocation stress on timeout names");
+
+  // Create a vector that will reallocate
+  std::vector names;
+  names.reserve(2);  // Small initial capacity to force reallocation
+
+  // Schedule callbacks with string names from vector
+  for (int i = 0; i < 10; i++) {
+    names.push_back("vector_cb_" + std::to_string(i));
+    // Use the string from vector as timeout name
+    this->set_timeout(names.back(), 8 + i * 1, [this, i]() {
+      ESP_LOGV(TAG, "Vector name callback %d executed", i);
+      this->tests_passed_++;
+    });
+  }
+
+  // Force reallocation by adding more elements
+  // This will move all strings to new memory locations
+  for (int i = 10; i < 50; i++) {
+    names.push_back("realloc_trigger_" + std::to_string(i));
+  }
+
+  // Add more timeouts after reallocation to ensure old names still work
+  for (int i = 50; i < 55; i++) {
+    names.push_back("post_realloc_" + std::to_string(i));
+    this->set_timeout(names.back(), 20 + (i - 50), [this]() {
+      ESP_LOGV(TAG, "Post-reallocation callback executed");
+      this->tests_passed_++;
+    });
+  }
+
+  // Clear the vector while timeouts are still pending
+  names.clear();
+  ESP_LOGD(TAG, "Vector cleared - all string names destroyed");
+}
+
+void SchedulerStringLifetimeComponent::test_string_move_semantics() {
+  ESP_LOGI(TAG, "Test 4: String move semantics for timeout names");
+
+  // Test moving string names
+  std::string original = "move_test_original";
+  std::string moved = std::move(original);
+
+  // Schedule with moved string as name
+  this->set_timeout(moved, 30, [this]() {
+    ESP_LOGD(TAG, "Moved string name callback executed");
+    this->tests_passed_++;
+  });
+
+  // original is now empty, try to use it as a different timeout name
+  original = "reused_after_move";
+  this->set_timeout(original, 32, [this]() {
+    ESP_LOGD(TAG, "Reused string name callback executed");
+    this->tests_passed_++;
+  });
+}
+
+void SchedulerStringLifetimeComponent::test_lambda_capture_lifetime() {
+  ESP_LOGI(TAG, "Test 5: Complex timeout name scenarios");
+
+  // Test scheduling with name built in lambda
+  [this]() {
+    std::string lambda_name = "lambda_built_name_" + std::to_string(888);
+    this->set_timeout(lambda_name, 38, [this]() {
+      ESP_LOGD(TAG, "Lambda-built name callback executed");
+      this->tests_passed_++;
+    });
+  }();  // Lambda executes and lambda_name is destroyed
+
+  // Test with shared_ptr name
+  auto shared_name = std::make_shared("shared_ptr_timeout");
+  this->set_timeout(*shared_name, 40, [this, shared_name]() {
+    ESP_LOGD(TAG, "Shared_ptr name callback executed");
+    this->tests_passed_++;
+  });
+  shared_name.reset();  // Release the shared_ptr
+
+  // Test overwriting timeout with same name
+  std::string overwrite_name = "overwrite_test";
+  this->set_timeout(overwrite_name, 1000, [this]() {
+    ESP_LOGE(TAG, "This should have been overwritten!");
+    this->tests_failed_++;
+  });
+
+  // Overwrite with shorter timeout
+  this->set_timeout(overwrite_name, 42, [this]() {
+    ESP_LOGD(TAG, "Overwritten timeout executed");
+    this->tests_passed_++;
+  });
+
+  // Test very long string name
+  std::string long_name;
+  for (int i = 0; i < 100; i++) {
+    long_name += "very_long_timeout_name_segment_" + std::to_string(i) + "_";
+  }
+  this->set_timeout(long_name, 44, [this]() {
+    ESP_LOGD(TAG, "Very long name timeout executed");
+    this->tests_passed_++;
+  });
+
+  // Test empty string as name
+  this->set_timeout("", 46, [this]() {
+    ESP_LOGD(TAG, "Empty string name timeout executed");
+    this->tests_passed_++;
+  });
+}
+
+}  // namespace scheduler_string_lifetime_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h
new file mode 100644
index 0000000000..95532328bb
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include 
+#include 
+
+namespace esphome {
+namespace scheduler_string_lifetime_component {
+
+class SchedulerStringLifetimeComponent : public Component {
+ public:
+  void setup() override;
+  float get_setup_priority() const override { return setup_priority::LATE; }
+
+  void run_string_lifetime_test();
+
+  // Individual test methods exposed as services
+  void run_test1();
+  void run_test2();
+  void run_test3();
+  void run_test4();
+  void run_test5();
+  void run_final_check();
+
+ private:
+  void test_temporary_string_lifetime();
+  void test_scope_exit_string();
+  void test_vector_reallocation();
+  void test_string_move_semantics();
+  void test_lambda_capture_lifetime();
+
+  int tests_passed_{0};
+  int tests_failed_{0};
+};
+
+}  // namespace scheduler_string_lifetime_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/__init__.py
new file mode 100644
index 0000000000..6cc564395c
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/__init__.py
@@ -0,0 +1,21 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+scheduler_string_name_stress_component_ns = cg.esphome_ns.namespace(
+    "scheduler_string_name_stress_component"
+)
+SchedulerStringNameStressComponent = scheduler_string_name_stress_component_ns.class_(
+    "SchedulerStringNameStressComponent", cg.Component
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(SchedulerStringNameStressComponent),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
diff --git a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp
new file mode 100644
index 0000000000..9071e573bb
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp
@@ -0,0 +1,110 @@
+#include "string_name_stress_component.h"
+#include "esphome/core/log.h"
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace esphome {
+namespace scheduler_string_name_stress_component {
+
+static const char *const TAG = "scheduler_string_name_stress";
+
+void SchedulerStringNameStressComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerStringNameStressComponent setup"); }
+
+void SchedulerStringNameStressComponent::run_string_name_stress_test() {
+  // Use member variables to reset state
+  this->total_callbacks_ = 0;
+  this->executed_callbacks_ = 0;
+  static constexpr int NUM_THREADS = 10;
+  static constexpr int CALLBACKS_PER_THREAD = 100;
+
+  ESP_LOGI(TAG, "Starting string name stress test - multi-threaded set_timeout with std::string names");
+  ESP_LOGI(TAG, "This test specifically uses dynamic string names to test memory management");
+
+  // Track start time
+  auto start_time = std::chrono::steady_clock::now();
+
+  // Create threads
+  std::vector threads;
+
+  ESP_LOGI(TAG, "Creating %d threads, each will schedule %d callbacks with dynamic names", NUM_THREADS,
+           CALLBACKS_PER_THREAD);
+
+  threads.reserve(NUM_THREADS);
+  for (int i = 0; i < NUM_THREADS; i++) {
+    threads.emplace_back([this, i]() {
+      ESP_LOGV(TAG, "Thread %d starting", i);
+
+      // Each thread schedules callbacks with dynamically created string names
+      for (int j = 0; j < CALLBACKS_PER_THREAD; j++) {
+        int callback_id = this->total_callbacks_.fetch_add(1);
+
+        // Create a dynamic string name - this will test memory management
+        std::stringstream ss;
+        ss << "thread_" << i << "_callback_" << j << "_id_" << callback_id;
+        std::string dynamic_name = ss.str();
+
+        ESP_LOGV(TAG, "Thread %d scheduling timeout with dynamic name: %s", i, dynamic_name.c_str());
+
+        // Capture necessary values for the lambda
+        auto *component = this;
+
+        // Schedule with std::string name - this tests the string overload
+        // Use varying delays to stress the heap scheduler
+        uint32_t delay = 1 + (callback_id % 50);
+
+        // Also test nested scheduling from callbacks
+        if (j % 10 == 0) {
+          // Every 10th callback schedules another callback
+          this->set_timeout(dynamic_name, delay, [component, callback_id]() {
+            component->executed_callbacks_.fetch_add(1);
+            ESP_LOGV(TAG, "Executed string-named callback %d (nested scheduler)", callback_id);
+
+            // Schedule another timeout from within this callback with a new dynamic name
+            std::string nested_name = "nested_from_" + std::to_string(callback_id);
+            component->set_timeout(nested_name, 1, [callback_id]() {
+              ESP_LOGV(TAG, "Executed nested string-named callback from %d", callback_id);
+            });
+          });
+        } else {
+          // Regular callback
+          this->set_timeout(dynamic_name, delay, [component, callback_id]() {
+            component->executed_callbacks_.fetch_add(1);
+            ESP_LOGV(TAG, "Executed string-named callback %d", callback_id);
+          });
+        }
+
+        // Add some timing variations to increase race conditions
+        if (j % 5 == 0) {
+          std::this_thread::sleep_for(std::chrono::microseconds(100));
+        }
+      }
+      ESP_LOGV(TAG, "Thread %d finished scheduling", i);
+    });
+  }
+
+  // Wait for all threads to complete scheduling
+  for (auto &t : threads) {
+    t.join();
+  }
+
+  auto end_time = std::chrono::steady_clock::now();
+  auto thread_time = std::chrono::duration_cast(end_time - start_time).count();
+  ESP_LOGI(TAG, "All threads finished scheduling in %lldms. Created %d callbacks with dynamic names", thread_time,
+           this->total_callbacks_.load());
+
+  // Give some time for callbacks to execute
+  ESP_LOGI(TAG, "Waiting for callbacks to execute...");
+
+  // Schedule a final callback to signal completion
+  this->set_timeout("test_complete", 2000, [this]() {
+    ESP_LOGI(TAG, "String name stress test complete. Executed %d of %d callbacks", this->executed_callbacks_.load(),
+             this->total_callbacks_.load());
+  });
+}
+
+}  // namespace scheduler_string_name_stress_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h
new file mode 100644
index 0000000000..002a0a7b51
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include 
+
+namespace esphome {
+namespace scheduler_string_name_stress_component {
+
+class SchedulerStringNameStressComponent : public Component {
+ public:
+  void setup() override;
+  float get_setup_priority() const override { return setup_priority::LATE; }
+
+  void run_string_name_stress_test();
+
+ private:
+  std::atomic total_callbacks_{0};
+  std::atomic executed_callbacks_{0};
+};
+
+}  // namespace scheduler_string_name_stress_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/scheduler_bulk_cleanup.yaml b/tests/integration/fixtures/scheduler_bulk_cleanup.yaml
new file mode 100644
index 0000000000..de876da8c4
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_bulk_cleanup.yaml
@@ -0,0 +1,23 @@
+esphome:
+  name: scheduler-bulk-cleanup
+
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH
+
+host:
+
+logger:
+  level: DEBUG
+
+api:
+  services:
+    - service: trigger_bulk_cleanup
+      then:
+        - lambda: |-
+            auto component = id(bulk_cleanup_component);
+            component->trigger_bulk_cleanup();
+
+scheduler_bulk_cleanup_component:
+  id: bulk_cleanup_component
diff --git a/tests/integration/fixtures/scheduler_defer_cancel.yaml b/tests/integration/fixtures/scheduler_defer_cancel.yaml
new file mode 100644
index 0000000000..9e3f927c33
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_defer_cancel.yaml
@@ -0,0 +1,51 @@
+esphome:
+  name: scheduler-defer-cancel
+
+host:
+
+logger:
+  level: DEBUG
+
+api:
+  services:
+    - service: test_defer_cancel
+      then:
+        - lambda: |-
+            // Schedule 10 defers with the same name
+            // Only the last one should execute
+            for (int i = 1; i <= 10; i++) {
+              App.scheduler.set_timeout(nullptr, "test_defer", 0, [i]() {
+                ESP_LOGI("TEST", "Defer executed: %d", i);
+                // Fire event with the defer number
+                std::string event_type = "defer_executed_" + std::to_string(i);
+                id(test_result)->trigger(event_type);
+              });
+            }
+
+            // Schedule completion notification after all defers
+            App.scheduler.set_timeout(nullptr, "completion", 0, []() {
+              ESP_LOGI("TEST", "Test complete");
+              id(test_complete)->trigger("test_finished");
+            });
+
+event:
+  - platform: template
+    id: test_result
+    name: "Test Result"
+    event_types:
+      - "defer_executed_1"
+      - "defer_executed_2"
+      - "defer_executed_3"
+      - "defer_executed_4"
+      - "defer_executed_5"
+      - "defer_executed_6"
+      - "defer_executed_7"
+      - "defer_executed_8"
+      - "defer_executed_9"
+      - "defer_executed_10"
+
+  - platform: template
+    id: test_complete
+    name: "Test Complete"
+    event_types:
+      - "test_finished"
diff --git a/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml b/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml
new file mode 100644
index 0000000000..fb6b1791dc
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml
@@ -0,0 +1,34 @@
+esphome:
+  name: scheduler-defer-cancel-regular
+
+host:
+
+logger:
+  level: DEBUG
+
+api:
+  services:
+    - service: test_defer_cancels_regular
+      then:
+        - lambda: |-
+            ESP_LOGI("TEST", "Starting defer cancels regular timeout test");
+
+            // Schedule a regular timeout with 100ms delay
+            App.scheduler.set_timeout(nullptr, "test_timeout", 100, []() {
+              ESP_LOGE("TEST", "ERROR: Regular timeout executed - should have been cancelled!");
+            });
+
+            ESP_LOGI("TEST", "Scheduled regular timeout with 100ms delay");
+
+            // Immediately schedule a deferred timeout (0 delay) with the same name
+            // This should cancel the regular timeout
+            App.scheduler.set_timeout(nullptr, "test_timeout", 0, []() {
+              ESP_LOGI("TEST", "SUCCESS: Deferred timeout executed");
+            });
+
+            ESP_LOGI("TEST", "Scheduled deferred timeout - should cancel regular timeout");
+
+            // Schedule test completion after 200ms (after regular timeout would have fired)
+            App.scheduler.set_timeout(nullptr, "test_complete", 200, []() {
+              ESP_LOGI("TEST", "Test complete");
+            });
diff --git a/tests/integration/fixtures/defer_fifo_simple.yaml b/tests/integration/fixtures/scheduler_defer_fifo_simple.yaml
similarity index 99%
rename from tests/integration/fixtures/defer_fifo_simple.yaml
rename to tests/integration/fixtures/scheduler_defer_fifo_simple.yaml
index db24ebf601..7384082ac2 100644
--- a/tests/integration/fixtures/defer_fifo_simple.yaml
+++ b/tests/integration/fixtures/scheduler_defer_fifo_simple.yaml
@@ -1,5 +1,5 @@
 esphome:
-  name: defer-fifo-simple
+  name: scheduler-defer-fifo-simple
 
 host:
 
diff --git a/tests/integration/fixtures/defer_stress.yaml b/tests/integration/fixtures/scheduler_defer_stress.yaml
similarity index 94%
rename from tests/integration/fixtures/defer_stress.yaml
rename to tests/integration/fixtures/scheduler_defer_stress.yaml
index 6df475229b..0d9c1d1405 100644
--- a/tests/integration/fixtures/defer_stress.yaml
+++ b/tests/integration/fixtures/scheduler_defer_stress.yaml
@@ -1,5 +1,5 @@
 esphome:
-  name: defer-stress-test
+  name: scheduler-defer-stress-test
 
 external_components:
   - source:
diff --git a/tests/integration/fixtures/scheduler_heap_stress.yaml b/tests/integration/fixtures/scheduler_heap_stress.yaml
new file mode 100644
index 0000000000..d4d340b68b
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_heap_stress.yaml
@@ -0,0 +1,38 @@
+esphome:
+  name: scheduler-heap-stress-test
+
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH
+    components: [scheduler_heap_stress_component]
+
+host:
+
+logger:
+  level: VERBOSE
+
+scheduler_heap_stress_component:
+  id: heap_stress
+
+api:
+  services:
+    - service: run_heap_stress_test
+      then:
+        - lambda: |-
+            id(heap_stress)->run_multi_thread_test();
+
+event:
+  - platform: template
+    name: "Test Complete"
+    id: test_complete
+    device_class: button
+    event_types:
+      - "test_finished"
+  - platform: template
+    name: "Test Result"
+    id: test_result
+    device_class: button
+    event_types:
+      - "passed"
+      - "failed"
diff --git a/tests/integration/fixtures/scheduler_rapid_cancellation.yaml b/tests/integration/fixtures/scheduler_rapid_cancellation.yaml
new file mode 100644
index 0000000000..4824654c5c
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_rapid_cancellation.yaml
@@ -0,0 +1,38 @@
+esphome:
+  name: sched-rapid-cancel-test
+
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH
+    components: [scheduler_rapid_cancellation_component]
+
+host:
+
+logger:
+  level: VERBOSE
+
+scheduler_rapid_cancellation_component:
+  id: rapid_cancel
+
+api:
+  services:
+    - service: run_rapid_cancellation_test
+      then:
+        - lambda: |-
+            id(rapid_cancel)->run_rapid_cancellation_test();
+
+event:
+  - platform: template
+    name: "Test Complete"
+    id: test_complete
+    device_class: button
+    event_types:
+      - "test_finished"
+  - platform: template
+    name: "Test Result"
+    id: test_result
+    device_class: button
+    event_types:
+      - "passed"
+      - "failed"
diff --git a/tests/integration/fixtures/scheduler_recursive_timeout.yaml b/tests/integration/fixtures/scheduler_recursive_timeout.yaml
new file mode 100644
index 0000000000..f1168802f6
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_recursive_timeout.yaml
@@ -0,0 +1,38 @@
+esphome:
+  name: sched-recursive-timeout
+
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH
+    components: [scheduler_recursive_timeout_component]
+
+host:
+
+logger:
+  level: VERBOSE
+
+scheduler_recursive_timeout_component:
+  id: recursive_timeout
+
+api:
+  services:
+    - service: run_recursive_timeout_test
+      then:
+        - lambda: |-
+            id(recursive_timeout)->run_recursive_timeout_test();
+
+event:
+  - platform: template
+    name: "Test Complete"
+    id: test_complete
+    device_class: button
+    event_types:
+      - "test_finished"
+  - platform: template
+    name: "Test Result"
+    id: test_result
+    device_class: button
+    event_types:
+      - "passed"
+      - "failed"
diff --git a/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml b/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml
new file mode 100644
index 0000000000..446ee7fdc0
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml
@@ -0,0 +1,23 @@
+esphome:
+  name: sched-simul-callbacks-test
+
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH
+    components: [scheduler_simultaneous_callbacks_component]
+
+host:
+
+logger:
+  level: INFO
+
+scheduler_simultaneous_callbacks_component:
+  id: simultaneous_callbacks
+
+api:
+  services:
+    - service: run_simultaneous_callbacks_test
+      then:
+        - lambda: |-
+            id(simultaneous_callbacks)->run_simultaneous_callbacks_test();
diff --git a/tests/integration/fixtures/scheduler_string_lifetime.yaml b/tests/integration/fixtures/scheduler_string_lifetime.yaml
new file mode 100644
index 0000000000..ebd5052b8b
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_string_lifetime.yaml
@@ -0,0 +1,47 @@
+esphome:
+  name: scheduler-string-lifetime-test
+
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH
+    components: [scheduler_string_lifetime_component]
+
+host:
+
+logger:
+  level: DEBUG
+
+scheduler_string_lifetime_component:
+  id: string_lifetime
+
+api:
+  services:
+    - service: run_string_lifetime_test
+      then:
+        - lambda: |-
+            id(string_lifetime)->run_string_lifetime_test();
+    - service: run_test1
+      then:
+        - lambda: |-
+            id(string_lifetime)->run_test1();
+    - service: run_test2
+      then:
+        - lambda: |-
+            id(string_lifetime)->run_test2();
+    - service: run_test3
+      then:
+        - lambda: |-
+            id(string_lifetime)->run_test3();
+    - service: run_test4
+      then:
+        - lambda: |-
+            id(string_lifetime)->run_test4();
+    - service: run_test5
+      then:
+        - lambda: |-
+            id(string_lifetime)->run_test5();
+    - service: run_final_check
+      then:
+        - lambda: |-
+            id(string_lifetime)->run_final_check();
diff --git a/tests/integration/fixtures/scheduler_string_name_stress.yaml b/tests/integration/fixtures/scheduler_string_name_stress.yaml
new file mode 100644
index 0000000000..d1ef55c8d5
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_string_name_stress.yaml
@@ -0,0 +1,38 @@
+esphome:
+  name: sched-string-name-stress
+
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH
+    components: [scheduler_string_name_stress_component]
+
+host:
+
+logger:
+  level: VERBOSE
+
+scheduler_string_name_stress_component:
+  id: string_stress
+
+api:
+  services:
+    - service: run_string_name_stress_test
+      then:
+        - lambda: |-
+            id(string_stress)->run_string_name_stress_test();
+
+event:
+  - platform: template
+    name: "Test Complete"
+    id: test_complete
+    device_class: button
+    event_types:
+      - "test_finished"
+  - platform: template
+    name: "Test Result"
+    id: test_result
+    device_class: button
+    event_types:
+      - "passed"
+      - "failed"
diff --git a/tests/integration/test_scheduler_bulk_cleanup.py b/tests/integration/test_scheduler_bulk_cleanup.py
new file mode 100644
index 0000000000..08ff293b84
--- /dev/null
+++ b/tests/integration/test_scheduler_bulk_cleanup.py
@@ -0,0 +1,122 @@
+"""Test that triggers the bulk cleanup path when to_remove_ > MAX_LOGICALLY_DELETED_ITEMS."""
+
+import asyncio
+from pathlib import Path
+import re
+
+from aioesphomeapi import UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_bulk_cleanup(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that bulk cleanup path is triggered when many items are cancelled."""
+
+    # Get the absolute path to the external components directory
+    external_components_path = str(
+        Path(__file__).parent / "fixtures" / "external_components"
+    )
+
+    # Replace the placeholder in the YAML config with the actual path
+    yaml_config = yaml_config.replace(
+        "EXTERNAL_COMPONENT_PATH", external_components_path
+    )
+
+    # Create a future to signal test completion
+    loop = asyncio.get_event_loop()
+    test_complete_future: asyncio.Future[None] = loop.create_future()
+    bulk_cleanup_triggered = False
+    cleanup_stats: dict[str, int] = {
+        "removed": 0,
+        "before": 0,
+        "after": 0,
+    }
+    post_cleanup_executed = 0
+
+    def on_log_line(line: str) -> None:
+        nonlocal bulk_cleanup_triggered, post_cleanup_executed
+
+        # Look for logs indicating bulk cleanup was triggered
+        # The actual cleanup happens silently, so we track the cancel operations
+        if "Successfully cancelled" in line and "timeouts" in line:
+            match = re.search(r"Successfully cancelled (\d+) timeouts", line)
+            if match and int(match.group(1)) > 10:
+                bulk_cleanup_triggered = True
+
+        # Track cleanup statistics
+        match = re.search(r"Bulk cleanup triggered: removed (\d+) items", line)
+        if match:
+            cleanup_stats["removed"] = int(match.group(1))
+
+        match = re.search(r"Items before cleanup: (\d+), after: (\d+)", line)
+        if match:
+            cleanup_stats["before"] = int(match.group(1))
+            cleanup_stats["after"] = int(match.group(2))
+
+        # Track post-cleanup timeout executions
+        if "Post-cleanup timeout" in line and "executed correctly" in line:
+            match = re.search(r"Post-cleanup timeout (\d+) executed correctly", line)
+            if match:
+                post_cleanup_executed += 1
+
+        # Check for final test completion
+        if (
+            "All post-cleanup timeouts completed - test finished" in line
+            and not test_complete_future.done()
+        ):
+            test_complete_future.set_result(None)
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "scheduler-bulk-cleanup"
+
+        # List entities and services
+        _, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test service
+        trigger_bulk_cleanup_service: UserService | None = None
+        for service in services:
+            if service.name == "trigger_bulk_cleanup":
+                trigger_bulk_cleanup_service = service
+                break
+
+        assert trigger_bulk_cleanup_service is not None, (
+            "trigger_bulk_cleanup service not found"
+        )
+
+        # Execute the test
+        client.execute_service(trigger_bulk_cleanup_service, {})
+
+        # Wait for test completion
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=10.0)
+        except asyncio.TimeoutError:
+            pytest.fail("Bulk cleanup test timed out")
+
+        # Verify bulk cleanup was triggered
+        assert bulk_cleanup_triggered, (
+            "Bulk cleanup path was not triggered - MAX_LOGICALLY_DELETED_ITEMS threshold not reached"
+        )
+
+        # Verify cleanup statistics
+        assert cleanup_stats["removed"] > 10, (
+            f"Expected more than 10 items removed, got {cleanup_stats['removed']}"
+        )
+
+        # Verify scheduler still works after bulk cleanup
+        assert post_cleanup_executed == 5, (
+            f"Expected 5 post-cleanup timeouts to execute, but {post_cleanup_executed} executed"
+        )
diff --git a/tests/integration/test_scheduler_defer_cancel.py b/tests/integration/test_scheduler_defer_cancel.py
new file mode 100644
index 0000000000..923cf946c4
--- /dev/null
+++ b/tests/integration/test_scheduler_defer_cancel.py
@@ -0,0 +1,94 @@
+"""Test that defer() with the same name cancels previous defers."""
+
+import asyncio
+
+from aioesphomeapi import EntityState, Event, EventInfo, UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_defer_cancel(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that defer() with the same name cancels previous defers."""
+
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "scheduler-defer-cancel"
+
+        # List entities and services
+        entity_info, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test entities
+        test_complete_entity: EventInfo | None = None
+        test_result_entity: EventInfo | None = None
+
+        for entity in entity_info:
+            if isinstance(entity, EventInfo):
+                if entity.object_id == "test_complete":
+                    test_complete_entity = entity
+                elif entity.object_id == "test_result":
+                    test_result_entity = entity
+
+        assert test_complete_entity is not None, "test_complete event not found"
+        assert test_result_entity is not None, "test_result event not found"
+
+        # Find our test service
+        test_defer_cancel_service: UserService | None = None
+        for service in services:
+            if service.name == "test_defer_cancel":
+                test_defer_cancel_service = service
+
+        assert test_defer_cancel_service is not None, (
+            "test_defer_cancel service not found"
+        )
+
+        # Get the event loop
+        loop = asyncio.get_running_loop()
+
+        # Subscribe to states
+        test_complete_future: asyncio.Future[bool] = loop.create_future()
+        test_result_future: asyncio.Future[int] = loop.create_future()
+
+        def on_state(state: EntityState) -> None:
+            if not isinstance(state, Event):
+                return
+
+            if (
+                state.key == test_complete_entity.key
+                and state.event_type == "test_finished"
+                and not test_complete_future.done()
+            ):
+                test_complete_future.set_result(True)
+                return
+
+            if state.key == test_result_entity.key and not test_result_future.done():
+                # Event type should be "defer_executed_X" where X is the defer number
+                if state.event_type.startswith("defer_executed_"):
+                    defer_num = int(state.event_type.split("_")[-1])
+                    test_result_future.set_result(defer_num)
+
+        client.subscribe_states(on_state)
+
+        # Execute the test
+        client.execute_service(test_defer_cancel_service, {})
+
+        # Wait for test completion
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=10.0)
+            executed_defer = await asyncio.wait_for(test_result_future, timeout=1.0)
+        except asyncio.TimeoutError:
+            pytest.fail("Test did not complete within timeout")
+
+        # Verify that only defer 10 was executed
+        assert executed_defer == 10, (
+            f"Expected defer 10 to execute, got {executed_defer}"
+        )
diff --git a/tests/integration/test_scheduler_defer_cancel_regular.py b/tests/integration/test_scheduler_defer_cancel_regular.py
new file mode 100644
index 0000000000..57b7134feb
--- /dev/null
+++ b/tests/integration/test_scheduler_defer_cancel_regular.py
@@ -0,0 +1,90 @@
+"""Test that a deferred timeout cancels a regular timeout with the same name."""
+
+import asyncio
+
+from aioesphomeapi import UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_defer_cancels_regular(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that set_timeout(name, 0) cancels a previously scheduled set_timeout(name, delay)."""
+
+    # Create a future to signal test completion
+    loop = asyncio.get_running_loop()
+    test_complete_future: asyncio.Future[None] = loop.create_future()
+
+    # Track log messages
+    log_messages: list[str] = []
+    error_detected = False
+
+    def on_log_line(line: str) -> None:
+        nonlocal error_detected
+        if "TEST" in line:
+            log_messages.append(line)
+
+        if "ERROR: Regular timeout executed" in line:
+            error_detected = True
+
+        if "Test complete" in line and not test_complete_future.done():
+            test_complete_future.set_result(None)
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "scheduler-defer-cancel-regular"
+
+        # List services
+        _, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test service
+        test_service: UserService | None = None
+        for service in services:
+            if service.name == "test_defer_cancels_regular":
+                test_service = service
+                break
+
+        assert test_service is not None, "test_defer_cancels_regular service not found"
+
+        # Execute the test
+        client.execute_service(test_service, {})
+
+        # Wait for test completion
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=5.0)
+        except asyncio.TimeoutError:
+            pytest.fail(f"Test timed out. Log messages: {log_messages}")
+
+        # Verify results
+        assert not error_detected, (
+            f"Regular timeout should have been cancelled but it executed! Logs: {log_messages}"
+        )
+
+        # Verify the deferred timeout executed
+        assert any(
+            "SUCCESS: Deferred timeout executed" in msg for msg in log_messages
+        ), f"Deferred timeout should have executed. Logs: {log_messages}"
+
+        # Verify the expected sequence of events
+        assert any(
+            "Starting defer cancels regular timeout test" in msg for msg in log_messages
+        )
+        assert any(
+            "Scheduled regular timeout with 100ms delay" in msg for msg in log_messages
+        )
+        assert any(
+            "Scheduled deferred timeout - should cancel regular timeout" in msg
+            for msg in log_messages
+        )
diff --git a/tests/integration/test_defer_fifo_simple.py b/tests/integration/test_scheduler_defer_fifo_simple.py
similarity index 97%
rename from tests/integration/test_defer_fifo_simple.py
rename to tests/integration/test_scheduler_defer_fifo_simple.py
index 5a62a45786..eb4058fedd 100644
--- a/tests/integration/test_defer_fifo_simple.py
+++ b/tests/integration/test_scheduler_defer_fifo_simple.py
@@ -9,7 +9,7 @@ from .types import APIClientConnectedFactory, RunCompiledFunction
 
 
 @pytest.mark.asyncio
-async def test_defer_fifo_simple(
+async def test_scheduler_defer_fifo_simple(
     yaml_config: str,
     run_compiled: RunCompiledFunction,
     api_client_connected: APIClientConnectedFactory,
@@ -20,7 +20,7 @@ async def test_defer_fifo_simple(
         # Verify we can connect
         device_info = await client.device_info()
         assert device_info is not None
-        assert device_info.name == "defer-fifo-simple"
+        assert device_info.name == "scheduler-defer-fifo-simple"
 
         # List entities and services
         entity_info, services = await asyncio.wait_for(
diff --git a/tests/integration/test_defer_stress.py b/tests/integration/test_scheduler_defer_stress.py
similarity index 97%
rename from tests/integration/test_defer_stress.py
rename to tests/integration/test_scheduler_defer_stress.py
index f63ec8d25f..d546b7132f 100644
--- a/tests/integration/test_defer_stress.py
+++ b/tests/integration/test_scheduler_defer_stress.py
@@ -11,7 +11,7 @@ from .types import APIClientConnectedFactory, RunCompiledFunction
 
 
 @pytest.mark.asyncio
-async def test_defer_stress(
+async def test_scheduler_defer_stress(
     yaml_config: str,
     run_compiled: RunCompiledFunction,
     api_client_connected: APIClientConnectedFactory,
@@ -75,7 +75,7 @@ async def test_defer_stress(
         # Verify we can connect
         device_info = await client.device_info()
         assert device_info is not None
-        assert device_info.name == "defer-stress-test"
+        assert device_info.name == "scheduler-defer-stress-test"
 
         # List entities and services
         entity_info, services = await asyncio.wait_for(
diff --git a/tests/integration/test_scheduler_heap_stress.py b/tests/integration/test_scheduler_heap_stress.py
new file mode 100644
index 0000000000..3c757bfc9d
--- /dev/null
+++ b/tests/integration/test_scheduler_heap_stress.py
@@ -0,0 +1,140 @@
+"""Stress test for heap scheduler thread safety with multiple threads."""
+
+import asyncio
+from pathlib import Path
+import re
+
+from aioesphomeapi import UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_heap_stress(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that set_timeout/set_interval doesn't crash when called rapidly from multiple threads."""
+
+    # Get the absolute path to the external components directory
+    external_components_path = str(
+        Path(__file__).parent / "fixtures" / "external_components"
+    )
+
+    # Replace the placeholder in the YAML config with the actual path
+    yaml_config = yaml_config.replace(
+        "EXTERNAL_COMPONENT_PATH", external_components_path
+    )
+
+    # Create a future to signal test completion
+    loop = asyncio.get_running_loop()
+    test_complete_future: asyncio.Future[None] = loop.create_future()
+
+    # Track executed timeouts/intervals and their order
+    executed_callbacks: set[int] = set()
+    thread_executions: dict[
+        int, list[int]
+    ] = {}  # thread_id -> list of indices in execution order
+    callback_types: dict[int, str] = {}  # callback_id -> "timeout" or "interval"
+
+    def on_log_line(line: str) -> None:
+        # Track all executed callbacks with thread and index info
+        match = re.search(
+            r"Executed (timeout|interval) (\d+) \(thread (\d+), index (\d+)\)", line
+        )
+        if not match:
+            # Also check for the completion message
+            if "All threads finished" in line and "Created 1000 callbacks" in line:
+                # Give scheduler some time to execute callbacks
+                pass
+            return
+
+        callback_type = match.group(1)
+        callback_id = int(match.group(2))
+        thread_id = int(match.group(3))
+        index = int(match.group(4))
+
+        # Only count each callback ID once (intervals might fire multiple times)
+        if callback_id not in executed_callbacks:
+            executed_callbacks.add(callback_id)
+            callback_types[callback_id] = callback_type
+
+        # Track execution order per thread
+        if thread_id not in thread_executions:
+            thread_executions[thread_id] = []
+
+        # Only append if this is a new execution for this thread
+        if index not in thread_executions[thread_id]:
+            thread_executions[thread_id].append(index)
+
+        # Check if we've executed all 1000 callbacks (0-999)
+        if len(executed_callbacks) >= 1000 and not test_complete_future.done():
+            test_complete_future.set_result(None)
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "scheduler-heap-stress-test"
+
+        # List entities and services
+        _, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test service
+        run_stress_test_service: UserService | None = None
+        for service in services:
+            if service.name == "run_heap_stress_test":
+                run_stress_test_service = service
+                break
+
+        assert run_stress_test_service is not None, (
+            "run_heap_stress_test service not found"
+        )
+
+        # Call the run_heap_stress_test service to start the test
+        client.execute_service(run_stress_test_service, {})
+
+        # Wait for all callbacks to execute (should be quick, but give more time for scheduling)
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=60.0)
+        except asyncio.TimeoutError:
+            # Report how many we got
+            pytest.fail(
+                f"Stress test timed out. Only {len(executed_callbacks)} of "
+                f"1000 callbacks executed. Missing IDs: "
+                f"{sorted(set(range(1000)) - executed_callbacks)[:10]}..."
+            )
+
+        # Verify all callbacks executed
+        assert len(executed_callbacks) == 1000, (
+            f"Expected 1000 callbacks, got {len(executed_callbacks)}"
+        )
+
+        # Verify we have all IDs from 0-999
+        expected_ids = set(range(1000))
+        missing_ids = expected_ids - executed_callbacks
+        assert not missing_ids, f"Missing callback IDs: {sorted(missing_ids)}"
+
+        # Verify we have a mix of timeouts and intervals
+        timeout_count = sum(1 for t in callback_types.values() if t == "timeout")
+        interval_count = sum(1 for t in callback_types.values() if t == "interval")
+        assert timeout_count > 0, "No timeouts were executed"
+        assert interval_count > 0, "No intervals were executed"
+
+        # Verify each thread executed callbacks
+        for thread_id, indices in thread_executions.items():
+            assert len(indices) == 100, (
+                f"Thread {thread_id} executed {len(indices)} callbacks, expected 100"
+            )
+        # Total should be 1000 callbacks
+        total_callbacks = timeout_count + interval_count
+        assert total_callbacks == 1000, (
+            f"Expected 1000 total callbacks but got {total_callbacks}"
+        )
diff --git a/tests/integration/test_scheduler_rapid_cancellation.py b/tests/integration/test_scheduler_rapid_cancellation.py
new file mode 100644
index 0000000000..90577f36f1
--- /dev/null
+++ b/tests/integration/test_scheduler_rapid_cancellation.py
@@ -0,0 +1,142 @@
+"""Rapid cancellation test - schedule and immediately cancel timeouts with string names."""
+
+import asyncio
+from pathlib import Path
+import re
+
+from aioesphomeapi import UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_rapid_cancellation(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test rapid schedule/cancel cycles that might expose race conditions."""
+
+    # Get the absolute path to the external components directory
+    external_components_path = str(
+        Path(__file__).parent / "fixtures" / "external_components"
+    )
+
+    # Replace the placeholder in the YAML config with the actual path
+    yaml_config = yaml_config.replace(
+        "EXTERNAL_COMPONENT_PATH", external_components_path
+    )
+
+    # Create a future to signal test completion
+    loop = asyncio.get_running_loop()
+    test_complete_future: asyncio.Future[None] = loop.create_future()
+
+    # Track test progress
+    test_stats = {
+        "log_count": 0,
+        "errors": [],
+        "summary_scheduled": None,
+        "final_scheduled": 0,
+        "final_executed": 0,
+        "final_implicit_cancellations": 0,
+    }
+
+    def on_log_line(line: str) -> None:
+        # Count log lines
+        test_stats["log_count"] += 1
+
+        # Check for errors (only ERROR level, not WARN)
+        if "ERROR" in line:
+            test_stats["errors"].append(line)
+
+        # Parse summary statistics
+        if "All threads completed. Scheduled:" in line:
+            # Extract the scheduled count from the summary
+            if match := re.search(r"Scheduled: (\d+)", line):
+                test_stats["summary_scheduled"] = int(match.group(1))
+        elif "Total scheduled:" in line:
+            if match := re.search(r"Total scheduled: (\d+)", line):
+                test_stats["final_scheduled"] = int(match.group(1))
+        elif "Total executed:" in line:
+            if match := re.search(r"Total executed: (\d+)", line):
+                test_stats["final_executed"] = int(match.group(1))
+        elif "Implicit cancellations (replaced):" in line:
+            if match := re.search(r"Implicit cancellations \(replaced\): (\d+)", line):
+                test_stats["final_implicit_cancellations"] = int(match.group(1))
+
+        # Check for crash indicators
+        if any(
+            indicator in line.lower()
+            for indicator in ["segfault", "abort", "assertion", "heap corruption"]
+        ):
+            if not test_complete_future.done():
+                test_complete_future.set_exception(Exception(f"Crash detected: {line}"))
+            return
+
+        # Check for completion - wait for final message after all stats are logged
+        if (
+            "Test finished - all statistics reported" in line
+            and not test_complete_future.done()
+        ):
+            test_complete_future.set_result(None)
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "sched-rapid-cancel-test"
+
+        # List entities and services
+        _, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test service
+        run_test_service: UserService | None = None
+        for service in services:
+            if service.name == "run_rapid_cancellation_test":
+                run_test_service = service
+                break
+
+        assert run_test_service is not None, (
+            "run_rapid_cancellation_test service not found"
+        )
+
+        # Call the service to start the test
+        client.execute_service(run_test_service, {})
+
+        # Wait for test to complete with timeout
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=10.0)
+        except asyncio.TimeoutError:
+            pytest.fail(f"Test timed out. Stats: {test_stats}")
+
+        # Check for any errors
+        assert len(test_stats["errors"]) == 0, (
+            f"Errors detected: {test_stats['errors']}"
+        )
+
+        # Check that we received log messages
+        assert test_stats["log_count"] > 0, "No log messages received"
+
+        # Check the summary line to verify all threads scheduled their operations
+        assert test_stats["summary_scheduled"] == 400, (
+            f"Expected summary to show 400 scheduled operations but got {test_stats['summary_scheduled']}"
+        )
+
+        # Check final statistics
+        assert test_stats["final_scheduled"] == 400, (
+            f"Expected final stats to show 400 scheduled but got {test_stats['final_scheduled']}"
+        )
+
+        assert test_stats["final_executed"] == 10, (
+            f"Expected final stats to show 10 executed but got {test_stats['final_executed']}"
+        )
+
+        assert test_stats["final_implicit_cancellations"] == 390, (
+            f"Expected final stats to show 390 implicit cancellations but got {test_stats['final_implicit_cancellations']}"
+        )
diff --git a/tests/integration/test_scheduler_recursive_timeout.py b/tests/integration/test_scheduler_recursive_timeout.py
new file mode 100644
index 0000000000..c015978e15
--- /dev/null
+++ b/tests/integration/test_scheduler_recursive_timeout.py
@@ -0,0 +1,101 @@
+"""Test for recursive timeout scheduling - scheduling timeouts from within timeout callbacks."""
+
+import asyncio
+from pathlib import Path
+
+from aioesphomeapi import UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_recursive_timeout(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that scheduling timeouts from within timeout callbacks works correctly."""
+
+    # Get the absolute path to the external components directory
+    external_components_path = str(
+        Path(__file__).parent / "fixtures" / "external_components"
+    )
+
+    # Replace the placeholder in the YAML config with the actual path
+    yaml_config = yaml_config.replace(
+        "EXTERNAL_COMPONENT_PATH", external_components_path
+    )
+
+    # Create a future to signal test completion
+    loop = asyncio.get_running_loop()
+    test_complete_future: asyncio.Future[None] = loop.create_future()
+
+    # Track execution sequence
+    execution_sequence: list[str] = []
+    expected_sequence = [
+        "initial_timeout",
+        "nested_timeout_1",
+        "nested_timeout_2",
+        "test_complete",
+    ]
+
+    def on_log_line(line: str) -> None:
+        # Track execution sequence
+        if "Executing initial timeout" in line:
+            execution_sequence.append("initial_timeout")
+        elif "Executing nested timeout 1" in line:
+            execution_sequence.append("nested_timeout_1")
+        elif "Executing nested timeout 2" in line:
+            execution_sequence.append("nested_timeout_2")
+        elif "Recursive timeout test complete" in line:
+            execution_sequence.append("test_complete")
+            if not test_complete_future.done():
+                test_complete_future.set_result(None)
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "sched-recursive-timeout"
+
+        # List entities and services
+        _, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test service
+        run_test_service: UserService | None = None
+        for service in services:
+            if service.name == "run_recursive_timeout_test":
+                run_test_service = service
+                break
+
+        assert run_test_service is not None, (
+            "run_recursive_timeout_test service not found"
+        )
+
+        # Call the service to start the test
+        client.execute_service(run_test_service, {})
+
+        # Wait for test to complete
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=10.0)
+        except asyncio.TimeoutError:
+            pytest.fail(
+                f"Recursive timeout test timed out. Got sequence: {execution_sequence}"
+            )
+
+        # Verify execution sequence
+        assert execution_sequence == expected_sequence, (
+            f"Execution sequence mismatch. Expected {expected_sequence}, "
+            f"got {execution_sequence}"
+        )
+
+        # Verify we got exactly 4 events (Initial + Level 1 + Level 2 + Complete)
+        assert len(execution_sequence) == 4, (
+            f"Expected 4 events but got {len(execution_sequence)}"
+        )
diff --git a/tests/integration/test_scheduler_simultaneous_callbacks.py b/tests/integration/test_scheduler_simultaneous_callbacks.py
new file mode 100644
index 0000000000..f5120ce4ce
--- /dev/null
+++ b/tests/integration/test_scheduler_simultaneous_callbacks.py
@@ -0,0 +1,123 @@
+"""Simultaneous callbacks test - schedule many callbacks for the same time from multiple threads."""
+
+import asyncio
+from pathlib import Path
+import re
+
+from aioesphomeapi import UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_simultaneous_callbacks(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test scheduling many callbacks for the exact same time from multiple threads."""
+
+    # Get the absolute path to the external components directory
+    external_components_path = str(
+        Path(__file__).parent / "fixtures" / "external_components"
+    )
+
+    # Replace the placeholder in the YAML config with the actual path
+    yaml_config = yaml_config.replace(
+        "EXTERNAL_COMPONENT_PATH", external_components_path
+    )
+
+    # Create a future to signal test completion
+    loop = asyncio.get_running_loop()
+    test_complete_future: asyncio.Future[None] = loop.create_future()
+
+    # Track test progress
+    test_stats = {
+        "scheduled": 0,
+        "executed": 0,
+        "expected": 1000,  # 10 threads * 100 callbacks
+        "errors": [],
+    }
+
+    def on_log_line(line: str) -> None:
+        # Track operations
+        if "Scheduled callback" in line:
+            test_stats["scheduled"] += 1
+        elif "Callback executed" in line:
+            test_stats["executed"] += 1
+        elif "ERROR" in line:
+            test_stats["errors"].append(line)
+
+        # Check for crash indicators
+        if any(
+            indicator in line.lower()
+            for indicator in ["segfault", "abort", "assertion", "heap corruption"]
+        ):
+            if not test_complete_future.done():
+                test_complete_future.set_exception(Exception(f"Crash detected: {line}"))
+            return
+
+        # Check for completion with final count
+        if "Final executed count:" in line:
+            # Extract number from log line like: "[07:59:47][I][simultaneous_callbacks:093]: Simultaneous callbacks test complete. Final executed count: 1000"
+            match = re.search(r"Final executed count:\s*(\d+)", line)
+            if match:
+                test_stats["final_count"] = int(match.group(1))
+
+        # Check for completion
+        if (
+            "Simultaneous callbacks test complete" in line
+            and not test_complete_future.done()
+        ):
+            test_complete_future.set_result(None)
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "sched-simul-callbacks-test"
+
+        # List entities and services
+        _, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test service
+        run_test_service: UserService | None = None
+        for service in services:
+            if service.name == "run_simultaneous_callbacks_test":
+                run_test_service = service
+                break
+
+        assert run_test_service is not None, (
+            "run_simultaneous_callbacks_test service not found"
+        )
+
+        # Call the service to start the test
+        client.execute_service(run_test_service, {})
+
+        # Wait for test to complete
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=30.0)
+        except asyncio.TimeoutError:
+            pytest.fail(f"Simultaneous callbacks test timed out. Stats: {test_stats}")
+
+        # Check for any errors
+        assert len(test_stats["errors"]) == 0, (
+            f"Errors detected: {test_stats['errors']}"
+        )
+
+        # Verify all callbacks executed using the final count from C++
+        final_count = test_stats.get("final_count", 0)
+        assert final_count == test_stats["expected"], (
+            f"Expected {test_stats['expected']} callbacks, but only {final_count} executed"
+        )
+
+        # The final_count is the authoritative count from the C++ component
+        assert final_count == 1000, (
+            f"Expected 1000 executed callbacks but got {final_count}"
+        )
diff --git a/tests/integration/test_scheduler_string_lifetime.py b/tests/integration/test_scheduler_string_lifetime.py
new file mode 100644
index 0000000000..4d77abd954
--- /dev/null
+++ b/tests/integration/test_scheduler_string_lifetime.py
@@ -0,0 +1,169 @@
+"""String lifetime test - verify scheduler handles string destruction correctly."""
+
+import asyncio
+from pathlib import Path
+import re
+
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_string_lifetime(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that scheduler correctly handles string lifetimes when strings go out of scope."""
+
+    # Get the absolute path to the external components directory
+    external_components_path = str(
+        Path(__file__).parent / "fixtures" / "external_components"
+    )
+
+    # Replace the placeholder in the YAML config with the actual path
+    yaml_config = yaml_config.replace(
+        "EXTERNAL_COMPONENT_PATH", external_components_path
+    )
+
+    # Create events for synchronization
+    test1_complete = asyncio.Event()
+    test2_complete = asyncio.Event()
+    test3_complete = asyncio.Event()
+    test4_complete = asyncio.Event()
+    test5_complete = asyncio.Event()
+    all_tests_complete = asyncio.Event()
+
+    # Track test progress
+    test_stats = {
+        "tests_passed": 0,
+        "tests_failed": 0,
+        "errors": [],
+        "current_test": None,
+        "test_callbacks_executed": {},
+    }
+
+    def on_log_line(line: str) -> None:
+        # Track test-specific events
+        if "Test 1 complete" in line:
+            test1_complete.set()
+        elif "Test 2 complete" in line:
+            test2_complete.set()
+        elif "Test 3 complete" in line:
+            test3_complete.set()
+        elif "Test 4 complete" in line:
+            test4_complete.set()
+        elif "Test 5 complete" in line:
+            test5_complete.set()
+
+        # Track individual callback executions
+        callback_match = re.search(r"Callback '(.+?)' executed", line)
+        if callback_match:
+            callback_name = callback_match.group(1)
+            test_stats["test_callbacks_executed"][callback_name] = True
+
+        # Track test results from the C++ test output
+        if "Tests passed:" in line and "string_lifetime" in line:
+            # Extract the number from "Tests passed: 32"
+            match = re.search(r"Tests passed:\s*(\d+)", line)
+            if match:
+                test_stats["tests_passed"] = int(match.group(1))
+        elif "Tests failed:" in line and "string_lifetime" in line:
+            match = re.search(r"Tests failed:\s*(\d+)", line)
+            if match:
+                test_stats["tests_failed"] = int(match.group(1))
+        elif "ERROR" in line and "string_lifetime" in line:
+            test_stats["errors"].append(line)
+
+        # Check for memory corruption indicators
+        if any(
+            indicator in line.lower()
+            for indicator in [
+                "use after free",
+                "heap corruption",
+                "segfault",
+                "abort",
+                "assertion",
+                "sanitizer",
+                "bad memory",
+                "invalid pointer",
+            ]
+        ):
+            pytest.fail(f"Memory corruption detected: {line}")
+
+        # Check for completion
+        if "String lifetime tests complete" in line:
+            all_tests_complete.set()
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "scheduler-string-lifetime-test"
+
+        # List entities and services
+        _, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test services
+        test_services = {}
+        for service in services:
+            if service.name == "run_test1":
+                test_services["test1"] = service
+            elif service.name == "run_test2":
+                test_services["test2"] = service
+            elif service.name == "run_test3":
+                test_services["test3"] = service
+            elif service.name == "run_test4":
+                test_services["test4"] = service
+            elif service.name == "run_test5":
+                test_services["test5"] = service
+            elif service.name == "run_final_check":
+                test_services["final"] = service
+
+        # Ensure all services are found
+        required_services = ["test1", "test2", "test3", "test4", "test5", "final"]
+        for service_name in required_services:
+            assert service_name in test_services, f"{service_name} service not found"
+
+        # Run tests sequentially, waiting for each to complete
+        try:
+            # Test 1
+            client.execute_service(test_services["test1"], {})
+            await asyncio.wait_for(test1_complete.wait(), timeout=5.0)
+
+            # Test 2
+            client.execute_service(test_services["test2"], {})
+            await asyncio.wait_for(test2_complete.wait(), timeout=5.0)
+
+            # Test 3
+            client.execute_service(test_services["test3"], {})
+            await asyncio.wait_for(test3_complete.wait(), timeout=5.0)
+
+            # Test 4
+            client.execute_service(test_services["test4"], {})
+            await asyncio.wait_for(test4_complete.wait(), timeout=5.0)
+
+            # Test 5
+            client.execute_service(test_services["test5"], {})
+            await asyncio.wait_for(test5_complete.wait(), timeout=5.0)
+
+            # Final check
+            client.execute_service(test_services["final"], {})
+            await asyncio.wait_for(all_tests_complete.wait(), timeout=5.0)
+
+        except asyncio.TimeoutError:
+            pytest.fail(f"String lifetime test timed out. Stats: {test_stats}")
+
+        # Check for any errors
+        assert test_stats["tests_failed"] == 0, f"Tests failed: {test_stats['errors']}"
+
+        # Verify we had the expected number of passing tests
+        assert test_stats["tests_passed"] == 30, (
+            f"Expected exactly 30 tests to pass, but got {test_stats['tests_passed']}"
+        )
diff --git a/tests/integration/test_scheduler_string_name_stress.py b/tests/integration/test_scheduler_string_name_stress.py
new file mode 100644
index 0000000000..3045842223
--- /dev/null
+++ b/tests/integration/test_scheduler_string_name_stress.py
@@ -0,0 +1,116 @@
+"""Stress test for heap scheduler with std::string names from multiple threads."""
+
+import asyncio
+from pathlib import Path
+import re
+
+from aioesphomeapi import UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_string_name_stress(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that set_timeout/set_interval with std::string names doesn't crash when called from multiple threads."""
+
+    # Get the absolute path to the external components directory
+    external_components_path = str(
+        Path(__file__).parent / "fixtures" / "external_components"
+    )
+
+    # Replace the placeholder in the YAML config with the actual path
+    yaml_config = yaml_config.replace(
+        "EXTERNAL_COMPONENT_PATH", external_components_path
+    )
+
+    # Create a future to signal test completion
+    loop = asyncio.get_running_loop()
+    test_complete_future: asyncio.Future[None] = loop.create_future()
+
+    # Track executed callbacks and any crashes
+    executed_callbacks: set[int] = set()
+    error_messages: list[str] = []
+
+    def on_log_line(line: str) -> None:
+        # Check for crash indicators
+        if any(
+            indicator in line.lower()
+            for indicator in [
+                "segfault",
+                "abort",
+                "assertion",
+                "heap corruption",
+                "use after free",
+            ]
+        ):
+            error_messages.append(line)
+            if not test_complete_future.done():
+                test_complete_future.set_exception(Exception(f"Crash detected: {line}"))
+            return
+
+        # Track executed callbacks
+        match = re.search(r"Executed string-named callback (\d+)", line)
+        if match:
+            callback_id = int(match.group(1))
+            executed_callbacks.add(callback_id)
+
+        # Check for completion
+        if (
+            "String name stress test complete" in line
+            and not test_complete_future.done()
+        ):
+            test_complete_future.set_result(None)
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "sched-string-name-stress"
+
+        # List entities and services
+        _, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test service
+        run_stress_test_service: UserService | None = None
+        for service in services:
+            if service.name == "run_string_name_stress_test":
+                run_stress_test_service = service
+                break
+
+        assert run_stress_test_service is not None, (
+            "run_string_name_stress_test service not found"
+        )
+
+        # Call the service to start the test
+        client.execute_service(run_stress_test_service, {})
+
+        # Wait for test to complete or crash
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=30.0)
+        except asyncio.TimeoutError:
+            pytest.fail(
+                f"String name stress test timed out. Executed {len(executed_callbacks)} callbacks. "
+                f"This might indicate a deadlock."
+            )
+
+        # Verify no errors occurred (crashes already handled by exception)
+        assert not error_messages, f"Errors detected during test: {error_messages}"
+
+        # Verify we executed all 1000 callbacks (10 threads × 100 callbacks each)
+        assert len(executed_callbacks) == 1000, (
+            f"Expected 1000 callbacks but got {len(executed_callbacks)}"
+        )
+
+        # Verify each callback ID was executed exactly once
+        for i in range(1000):
+            assert i in executed_callbacks, f"Callback {i} was not executed"

From 31f36df4ba1c42e59eac9e148c76ded1783739bf Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 15:20:40 -0500
Subject: [PATCH 261/293] Reduce LightCall memory usage by 50 bytes per call
 (#9333)

---
 esphome/components/light/addressable_light.h  |   6 +-
 .../components/light/esp_color_correction.h   |   2 +-
 esphome/components/light/light_call.cpp       | 383 ++++++++----------
 esphome/components/light/light_call.h         |  92 ++++-
 esphome/components/light/light_color_values.h |   8 +-
 esphome/components/light/light_state.h        |  21 +-
 esphome/components/light/transformers.h       |   4 +-
 tests/integration/fixtures/light_calls.yaml   |  80 ++++
 tests/integration/test_light_calls.py         | 189 +++++++++
 9 files changed, 522 insertions(+), 263 deletions(-)
 create mode 100644 tests/integration/fixtures/light_calls.yaml
 create mode 100644 tests/integration/test_light_calls.py

diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h
index 8302239d6a..baa4507d2f 100644
--- a/esphome/components/light/addressable_light.h
+++ b/esphome/components/light/addressable_light.h
@@ -97,12 +97,12 @@ class AddressableLight : public LightOutput, public Component {
   }
   virtual ESPColorView get_view_internal(int32_t index) const = 0;
 
-  bool effect_active_{false};
   ESPColorCorrection correction_{};
+  LightState *state_parent_{nullptr};
 #ifdef USE_POWER_SUPPLY
   power_supply::PowerSupplyRequester power_;
 #endif
-  LightState *state_parent_{nullptr};
+  bool effect_active_{false};
 };
 
 class AddressableLightTransformer : public LightTransitionTransformer {
@@ -114,9 +114,9 @@ class AddressableLightTransformer : public LightTransitionTransformer {
 
  protected:
   AddressableLight &light_;
-  Color target_color_{};
   float last_transition_progress_{0.0f};
   float accumulated_alpha_{0.0f};
+  Color target_color_{};
 };
 
 }  // namespace light
diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h
index 39ce5700c6..979a1acb07 100644
--- a/esphome/components/light/esp_color_correction.h
+++ b/esphome/components/light/esp_color_correction.h
@@ -69,8 +69,8 @@ class ESPColorCorrection {
  protected:
   uint8_t gamma_table_[256];
   uint8_t gamma_reverse_table_[256];
-  uint8_t local_brightness_{255};
   Color max_brightness_;
+  uint8_t local_brightness_{255};
 };
 
 }  // namespace light
diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp
index 78b0ac9feb..a3ffe22591 100644
--- a/esphome/components/light/light_call.cpp
+++ b/esphome/components/light/light_call.cpp
@@ -2,12 +2,28 @@
 #include "light_call.h"
 #include "light_state.h"
 #include "esphome/core/log.h"
+#include "esphome/core/optional.h"
 
 namespace esphome {
 namespace light {
 
 static const char *const TAG = "light";
 
+// Macro to reduce repetitive setter code
+#define IMPLEMENT_LIGHT_CALL_SETTER(name, type, flag) \
+  LightCall &LightCall::set_##name(optional(name)) { \
+    if ((name).has_value()) { \
+      this->name##_ = (name).value(); \
+    } \
+    this->set_flag_(flag, (name).has_value()); \
+    return *this; \
+  } \
+  LightCall &LightCall::set_##name(type name) { \
+    this->name##_ = name; \
+    this->set_flag_(flag, true); \
+    return *this; \
+  }
+
 static const LogString *color_mode_to_human(ColorMode color_mode) {
   if (color_mode == ColorMode::UNKNOWN)
     return LOG_STR("Unknown");
@@ -32,41 +48,43 @@ void LightCall::perform() {
   const char *name = this->parent_->get_name().c_str();
   LightColorValues v = this->validate_();
 
-  if (this->publish_) {
+  if (this->get_publish_()) {
     ESP_LOGD(TAG, "'%s' Setting:", name);
 
     // Only print color mode when it's being changed
     ColorMode current_color_mode = this->parent_->remote_values.get_color_mode();
-    if (this->color_mode_.value_or(current_color_mode) != current_color_mode) {
+    ColorMode target_color_mode = this->has_color_mode() ? this->color_mode_ : current_color_mode;
+    if (target_color_mode != current_color_mode) {
       ESP_LOGD(TAG, "  Color mode: %s", LOG_STR_ARG(color_mode_to_human(v.get_color_mode())));
     }
 
     // Only print state when it's being changed
     bool current_state = this->parent_->remote_values.is_on();
-    if (this->state_.value_or(current_state) != current_state) {
+    bool target_state = this->has_state() ? this->state_ : current_state;
+    if (target_state != current_state) {
       ESP_LOGD(TAG, "  State: %s", ONOFF(v.is_on()));
     }
 
-    if (this->brightness_.has_value()) {
+    if (this->has_brightness()) {
       ESP_LOGD(TAG, "  Brightness: %.0f%%", v.get_brightness() * 100.0f);
     }
 
-    if (this->color_brightness_.has_value()) {
+    if (this->has_color_brightness()) {
       ESP_LOGD(TAG, "  Color brightness: %.0f%%", v.get_color_brightness() * 100.0f);
     }
-    if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) {
+    if (this->has_red() || this->has_green() || this->has_blue()) {
       ESP_LOGD(TAG, "  Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f,
                v.get_blue() * 100.0f);
     }
 
-    if (this->white_.has_value()) {
+    if (this->has_white()) {
       ESP_LOGD(TAG, "  White: %.0f%%", v.get_white() * 100.0f);
     }
-    if (this->color_temperature_.has_value()) {
+    if (this->has_color_temperature()) {
       ESP_LOGD(TAG, "  Color temperature: %.1f mireds", v.get_color_temperature());
     }
 
-    if (this->cold_white_.has_value() || this->warm_white_.has_value()) {
+    if (this->has_cold_white() || this->has_warm_white()) {
       ESP_LOGD(TAG, "  Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f,
                v.get_warm_white() * 100.0f);
     }
@@ -74,58 +92,57 @@ void LightCall::perform() {
 
   if (this->has_flash_()) {
     // FLASH
-    if (this->publish_) {
-      ESP_LOGD(TAG, "  Flash length: %.1fs", *this->flash_length_ / 1e3f);
+    if (this->get_publish_()) {
+      ESP_LOGD(TAG, "  Flash length: %.1fs", this->flash_length_ / 1e3f);
     }
 
-    this->parent_->start_flash_(v, *this->flash_length_, this->publish_);
+    this->parent_->start_flash_(v, this->flash_length_, this->get_publish_());
   } else if (this->has_transition_()) {
     // TRANSITION
-    if (this->publish_) {
-      ESP_LOGD(TAG, "  Transition length: %.1fs", *this->transition_length_ / 1e3f);
+    if (this->get_publish_()) {
+      ESP_LOGD(TAG, "  Transition length: %.1fs", this->transition_length_ / 1e3f);
     }
 
     // Special case: Transition and effect can be set when turning off
     if (this->has_effect_()) {
-      if (this->publish_) {
+      if (this->get_publish_()) {
         ESP_LOGD(TAG, "  Effect: 'None'");
       }
       this->parent_->stop_effect_();
     }
 
-    this->parent_->start_transition_(v, *this->transition_length_, this->publish_);
+    this->parent_->start_transition_(v, this->transition_length_, this->get_publish_());
 
   } else if (this->has_effect_()) {
     // EFFECT
-    auto effect = this->effect_;
     const char *effect_s;
-    if (effect == 0u) {
+    if (this->effect_ == 0u) {
       effect_s = "None";
     } else {
-      effect_s = this->parent_->effects_[*this->effect_ - 1]->get_name().c_str();
+      effect_s = this->parent_->effects_[this->effect_ - 1]->get_name().c_str();
     }
 
-    if (this->publish_) {
+    if (this->get_publish_()) {
       ESP_LOGD(TAG, "  Effect: '%s'", effect_s);
     }
 
-    this->parent_->start_effect_(*this->effect_);
+    this->parent_->start_effect_(this->effect_);
 
     // Also set light color values when starting an effect
     // For example to turn off the light
     this->parent_->set_immediately_(v, true);
   } else {
     // INSTANT CHANGE
-    this->parent_->set_immediately_(v, this->publish_);
+    this->parent_->set_immediately_(v, this->get_publish_());
   }
 
   if (!this->has_transition_()) {
     this->parent_->target_state_reached_callback_.call();
   }
-  if (this->publish_) {
+  if (this->get_publish_()) {
     this->parent_->publish_state();
   }
-  if (this->save_) {
+  if (this->get_save_()) {
     this->parent_->save_remote_values_();
   }
 }
@@ -135,82 +152,80 @@ LightColorValues LightCall::validate_() {
   auto traits = this->parent_->get_traits();
 
   // Color mode check
-  if (this->color_mode_.has_value() && !traits.supports_color_mode(this->color_mode_.value())) {
-    ESP_LOGW(TAG, "'%s' does not support color mode %s", name,
-             LOG_STR_ARG(color_mode_to_human(this->color_mode_.value())));
-    this->color_mode_.reset();
+  if (this->has_color_mode() && !traits.supports_color_mode(this->color_mode_)) {
+    ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_)));
+    this->set_flag_(FLAG_HAS_COLOR_MODE, false);
   }
 
   // Ensure there is always a color mode set
-  if (!this->color_mode_.has_value()) {
+  if (!this->has_color_mode()) {
     this->color_mode_ = this->compute_color_mode_();
+    this->set_flag_(FLAG_HAS_COLOR_MODE, true);
   }
-  auto color_mode = *this->color_mode_;
+  auto color_mode = this->color_mode_;
 
   // Transform calls that use non-native parameters for the current mode.
   this->transform_parameters_();
 
   // Brightness exists check
-  if (this->brightness_.has_value() && *this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) {
+  if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) {
     ESP_LOGW(TAG, "'%s': setting brightness not supported", name);
-    this->brightness_.reset();
+    this->set_flag_(FLAG_HAS_BRIGHTNESS, false);
   }
 
   // Transition length possible check
-  if (this->transition_length_.has_value() && *this->transition_length_ != 0 &&
-      !(color_mode & ColorCapability::BRIGHTNESS)) {
+  if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) {
     ESP_LOGW(TAG, "'%s': transitions not supported", name);
-    this->transition_length_.reset();
+    this->set_flag_(FLAG_HAS_TRANSITION, false);
   }
 
   // Color brightness exists check
-  if (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) {
+  if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) {
     ESP_LOGW(TAG, "'%s': color mode does not support setting RGB brightness", name);
-    this->color_brightness_.reset();
+    this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, false);
   }
 
   // RGB exists check
-  if ((this->red_.has_value() && *this->red_ > 0.0f) || (this->green_.has_value() && *this->green_ > 0.0f) ||
-      (this->blue_.has_value() && *this->blue_ > 0.0f)) {
+  if ((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) ||
+      (this->has_blue() && this->blue_ > 0.0f)) {
     if (!(color_mode & ColorCapability::RGB)) {
       ESP_LOGW(TAG, "'%s': color mode does not support setting RGB color", name);
-      this->red_.reset();
-      this->green_.reset();
-      this->blue_.reset();
+      this->set_flag_(FLAG_HAS_RED, false);
+      this->set_flag_(FLAG_HAS_GREEN, false);
+      this->set_flag_(FLAG_HAS_BLUE, false);
     }
   }
 
   // White value exists check
-  if (this->white_.has_value() && *this->white_ > 0.0f &&
+  if (this->has_white() && this->white_ > 0.0f &&
       !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) {
     ESP_LOGW(TAG, "'%s': color mode does not support setting white value", name);
-    this->white_.reset();
+    this->set_flag_(FLAG_HAS_WHITE, false);
   }
 
   // Color temperature exists check
-  if (this->color_temperature_.has_value() &&
+  if (this->has_color_temperature() &&
       !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) {
     ESP_LOGW(TAG, "'%s': color mode does not support setting color temperature", name);
-    this->color_temperature_.reset();
+    this->set_flag_(FLAG_HAS_COLOR_TEMPERATURE, false);
   }
 
   // Cold/warm white value exists check
-  if ((this->cold_white_.has_value() && *this->cold_white_ > 0.0f) ||
-      (this->warm_white_.has_value() && *this->warm_white_ > 0.0f)) {
+  if ((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) {
     if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) {
       ESP_LOGW(TAG, "'%s': color mode does not support setting cold/warm white value", name);
-      this->cold_white_.reset();
-      this->warm_white_.reset();
+      this->set_flag_(FLAG_HAS_COLD_WHITE, false);
+      this->set_flag_(FLAG_HAS_WARM_WHITE, false);
     }
   }
 
 #define VALIDATE_RANGE_(name_, upper_name, min, max) \
-  if (name_##_.has_value()) { \
-    auto val = *name_##_; \
+  if (this->has_##name_()) { \
+    auto val = this->name_##_; \
     if (val < (min) || val > (max)) { \
       ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_LITERAL(upper_name), val, \
                (min), (max)); \
-      name_##_ = clamp(val, (min), (max)); \
+      this->name_##_ = clamp(val, (min), (max)); \
     } \
   }
 #define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name, 0.0f, 1.0f)
@@ -227,110 +242,116 @@ LightColorValues LightCall::validate_() {
   VALIDATE_RANGE_(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds())
 
   // Flag whether an explicit turn off was requested, in which case we'll also stop the effect.
-  bool explicit_turn_off_request = this->state_.has_value() && !*this->state_;
+  bool explicit_turn_off_request = this->has_state() && !this->state_;
 
   // Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on).
-  if (this->brightness_.has_value() && *this->brightness_ == 0.0f) {
-    this->state_ = optional(false);
-    this->brightness_ = optional(1.0f);
+  if (this->has_brightness() && this->brightness_ == 0.0f) {
+    this->state_ = false;
+    this->set_flag_(FLAG_HAS_STATE, true);
+    this->brightness_ = 1.0f;
   }
 
   // Set color brightness to 100% if currently zero and a color is set.
-  if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) {
-    if (!this->color_brightness_.has_value() && this->parent_->remote_values.get_color_brightness() == 0.0f)
-      this->color_brightness_ = optional(1.0f);
+  if (this->has_red() || this->has_green() || this->has_blue()) {
+    if (!this->has_color_brightness() && this->parent_->remote_values.get_color_brightness() == 0.0f) {
+      this->color_brightness_ = 1.0f;
+      this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, true);
+    }
   }
 
   // Create color values for the light with this call applied.
   auto v = this->parent_->remote_values;
-  if (this->color_mode_.has_value())
-    v.set_color_mode(*this->color_mode_);
-  if (this->state_.has_value())
-    v.set_state(*this->state_);
-  if (this->brightness_.has_value())
-    v.set_brightness(*this->brightness_);
-  if (this->color_brightness_.has_value())
-    v.set_color_brightness(*this->color_brightness_);
-  if (this->red_.has_value())
-    v.set_red(*this->red_);
-  if (this->green_.has_value())
-    v.set_green(*this->green_);
-  if (this->blue_.has_value())
-    v.set_blue(*this->blue_);
-  if (this->white_.has_value())
-    v.set_white(*this->white_);
-  if (this->color_temperature_.has_value())
-    v.set_color_temperature(*this->color_temperature_);
-  if (this->cold_white_.has_value())
-    v.set_cold_white(*this->cold_white_);
-  if (this->warm_white_.has_value())
-    v.set_warm_white(*this->warm_white_);
+  if (this->has_color_mode())
+    v.set_color_mode(this->color_mode_);
+  if (this->has_state())
+    v.set_state(this->state_);
+  if (this->has_brightness())
+    v.set_brightness(this->brightness_);
+  if (this->has_color_brightness())
+    v.set_color_brightness(this->color_brightness_);
+  if (this->has_red())
+    v.set_red(this->red_);
+  if (this->has_green())
+    v.set_green(this->green_);
+  if (this->has_blue())
+    v.set_blue(this->blue_);
+  if (this->has_white())
+    v.set_white(this->white_);
+  if (this->has_color_temperature())
+    v.set_color_temperature(this->color_temperature_);
+  if (this->has_cold_white())
+    v.set_cold_white(this->cold_white_);
+  if (this->has_warm_white())
+    v.set_warm_white(this->warm_white_);
 
   v.normalize_color();
 
   // Flash length check
-  if (this->has_flash_() && *this->flash_length_ == 0) {
+  if (this->has_flash_() && this->flash_length_ == 0) {
     ESP_LOGW(TAG, "'%s': flash length must be greater than zero", name);
-    this->flash_length_.reset();
+    this->set_flag_(FLAG_HAS_FLASH, false);
   }
 
   // validate transition length/flash length/effect not used at the same time
   bool supports_transition = color_mode & ColorCapability::BRIGHTNESS;
 
   // If effect is already active, remove effect start
-  if (this->has_effect_() && *this->effect_ == this->parent_->active_effect_index_) {
-    this->effect_.reset();
+  if (this->has_effect_() && this->effect_ == this->parent_->active_effect_index_) {
+    this->set_flag_(FLAG_HAS_EFFECT, false);
   }
 
   // validate effect index
-  if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) {
-    ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, *this->effect_);
-    this->effect_.reset();
+  if (this->has_effect_() && this->effect_ > this->parent_->effects_.size()) {
+    ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, this->effect_);
+    this->set_flag_(FLAG_HAS_EFFECT, false);
   }
 
   if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) {
     ESP_LOGW(TAG, "'%s': effect cannot be used with transition/flash", name);
-    this->transition_length_.reset();
-    this->flash_length_.reset();
+    this->set_flag_(FLAG_HAS_TRANSITION, false);
+    this->set_flag_(FLAG_HAS_FLASH, false);
   }
 
   if (this->has_flash_() && this->has_transition_()) {
     ESP_LOGW(TAG, "'%s': flash cannot be used with transition", name);
-    this->transition_length_.reset();
+    this->set_flag_(FLAG_HAS_TRANSITION, false);
   }
 
-  if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || *this->effect_ == 0) &&
+  if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || this->effect_ == 0) &&
       supports_transition) {
     // nothing specified and light supports transitions, set default transition length
     this->transition_length_ = this->parent_->default_transition_length_;
+    this->set_flag_(FLAG_HAS_TRANSITION, true);
   }
 
-  if (this->transition_length_.value_or(0) == 0) {
+  if (this->has_transition_() && this->transition_length_ == 0) {
     // 0 transition is interpreted as no transition (instant change)
-    this->transition_length_.reset();
+    this->set_flag_(FLAG_HAS_TRANSITION, false);
   }
 
   if (this->has_transition_() && !supports_transition) {
     ESP_LOGW(TAG, "'%s': transitions not supported", name);
-    this->transition_length_.reset();
+    this->set_flag_(FLAG_HAS_TRANSITION, false);
   }
 
   // If not a flash and turning the light off, then disable the light
   // Do not use light color values directly, so that effects can set 0% brightness
   // Reason: When user turns off the light in frontend, the effect should also stop
-  if (!this->has_flash_() && !this->state_.value_or(v.is_on())) {
+  bool target_state = this->has_state() ? this->state_ : v.is_on();
+  if (!this->has_flash_() && !target_state) {
     if (this->has_effect_()) {
       ESP_LOGW(TAG, "'%s': cannot start effect when turning off", name);
-      this->effect_.reset();
+      this->set_flag_(FLAG_HAS_EFFECT, false);
     } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) {
       // Auto turn off effect
       this->effect_ = 0;
+      this->set_flag_(FLAG_HAS_EFFECT, true);
     }
   }
 
   // Disable saving for flashes
   if (this->has_flash_())
-    this->save_ = false;
+    this->set_flag_(FLAG_SAVE, false);
 
   return v;
 }
@@ -343,24 +364,27 @@ void LightCall::transform_parameters_() {
   // - RGBWW lights with color_interlock=true, which also sets "brightness" and
   //   "color_temperature" (without color_interlock, CW/WW are set directly)
   // - Legacy Home Assistant (pre-colormode), which sets "white" and "color_temperature"
-  if (((this->white_.has_value() && *this->white_ > 0.0f) || this->color_temperature_.has_value()) &&  //
-      (*this->color_mode_ & ColorCapability::COLD_WARM_WHITE) &&                                       //
-      !(*this->color_mode_ & ColorCapability::WHITE) &&                                                //
-      !(*this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) &&                                    //
+  if (((this->has_white() && this->white_ > 0.0f) || this->has_color_temperature()) &&  //
+      (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) &&                         //
+      !(this->color_mode_ & ColorCapability::WHITE) &&                                  //
+      !(this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) &&                      //
       traits.get_min_mireds() > 0.0f && traits.get_max_mireds() > 0.0f) {
     ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values",
              this->parent_->get_name().c_str());
-    if (this->color_temperature_.has_value()) {
-      const float color_temp = clamp(*this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds());
+    if (this->has_color_temperature()) {
+      const float color_temp = clamp(this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds());
       const float ww_fraction =
           (color_temp - traits.get_min_mireds()) / (traits.get_max_mireds() - traits.get_min_mireds());
       const float cw_fraction = 1.0f - ww_fraction;
       const float max_cw_ww = std::max(ww_fraction, cw_fraction);
       this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, this->parent_->get_gamma_correct());
       this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, this->parent_->get_gamma_correct());
+      this->set_flag_(FLAG_HAS_COLD_WHITE, true);
+      this->set_flag_(FLAG_HAS_WARM_WHITE, true);
     }
-    if (this->white_.has_value()) {
-      this->brightness_ = *this->white_;
+    if (this->has_white()) {
+      this->brightness_ = this->white_;
+      this->set_flag_(FLAG_HAS_BRIGHTNESS, true);
     }
   }
 }
@@ -378,7 +402,7 @@ ColorMode LightCall::compute_color_mode_() {
 
   // Don't change if the light is being turned off.
   ColorMode current_mode = this->parent_->remote_values.get_color_mode();
-  if (this->state_.has_value() && !*this->state_)
+  if (this->has_state() && !this->state_)
     return current_mode;
 
   // If no color mode is specified, we try to guess the color mode. This is needed for backward compatibility to
@@ -411,12 +435,12 @@ ColorMode LightCall::compute_color_mode_() {
   return color_mode;
 }
 std::set LightCall::get_suitable_color_modes_() {
-  bool has_white = this->white_.has_value() && *this->white_ > 0.0f;
-  bool has_ct = this->color_temperature_.has_value();
-  bool has_cwww = (this->cold_white_.has_value() && *this->cold_white_ > 0.0f) ||
-                  (this->warm_white_.has_value() && *this->warm_white_ > 0.0f);
-  bool has_rgb = (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f) ||
-                 (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value());
+  bool has_white = this->has_white() && this->white_ > 0.0f;
+  bool has_ct = this->has_color_temperature();
+  bool has_cwww =
+      (this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f);
+  bool has_rgb = (this->has_color_brightness() && this->color_brightness_ > 0.0f) ||
+                 (this->has_red() || this->has_green() || this->has_blue());
 
 #define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3)
 #define ENTRY(white, ct, cwww, rgb, ...) \
@@ -491,7 +515,7 @@ LightCall &LightCall::from_light_color_values(const LightColorValues &values) {
   return *this;
 }
 ColorMode LightCall::get_active_color_mode_() {
-  return this->color_mode_.value_or(this->parent_->remote_values.get_color_mode());
+  return this->has_color_mode() ? this->color_mode_ : this->parent_->remote_values.get_color_mode();
 }
 LightCall &LightCall::set_transition_length_if_supported(uint32_t transition_length) {
   if (this->get_active_color_mode_() & ColorCapability::BRIGHTNESS)
@@ -505,7 +529,7 @@ LightCall &LightCall::set_brightness_if_supported(float brightness) {
 }
 LightCall &LightCall::set_color_mode_if_supported(ColorMode color_mode) {
   if (this->parent_->get_traits().supports_color_mode(color_mode))
-    this->color_mode_ = color_mode;
+    this->set_color_mode(color_mode);
   return *this;
 }
 LightCall &LightCall::set_color_brightness_if_supported(float brightness) {
@@ -549,110 +573,19 @@ LightCall &LightCall::set_warm_white_if_supported(float warm_white) {
     this->set_warm_white(warm_white);
   return *this;
 }
-LightCall &LightCall::set_state(optional state) {
-  this->state_ = state;
-  return *this;
-}
-LightCall &LightCall::set_state(bool state) {
-  this->state_ = state;
-  return *this;
-}
-LightCall &LightCall::set_transition_length(optional transition_length) {
-  this->transition_length_ = transition_length;
-  return *this;
-}
-LightCall &LightCall::set_transition_length(uint32_t transition_length) {
-  this->transition_length_ = transition_length;
-  return *this;
-}
-LightCall &LightCall::set_flash_length(optional flash_length) {
-  this->flash_length_ = flash_length;
-  return *this;
-}
-LightCall &LightCall::set_flash_length(uint32_t flash_length) {
-  this->flash_length_ = flash_length;
-  return *this;
-}
-LightCall &LightCall::set_brightness(optional brightness) {
-  this->brightness_ = brightness;
-  return *this;
-}
-LightCall &LightCall::set_brightness(float brightness) {
-  this->brightness_ = brightness;
-  return *this;
-}
-LightCall &LightCall::set_color_mode(optional color_mode) {
-  this->color_mode_ = color_mode;
-  return *this;
-}
-LightCall &LightCall::set_color_mode(ColorMode color_mode) {
-  this->color_mode_ = color_mode;
-  return *this;
-}
-LightCall &LightCall::set_color_brightness(optional brightness) {
-  this->color_brightness_ = brightness;
-  return *this;
-}
-LightCall &LightCall::set_color_brightness(float brightness) {
-  this->color_brightness_ = brightness;
-  return *this;
-}
-LightCall &LightCall::set_red(optional red) {
-  this->red_ = red;
-  return *this;
-}
-LightCall &LightCall::set_red(float red) {
-  this->red_ = red;
-  return *this;
-}
-LightCall &LightCall::set_green(optional green) {
-  this->green_ = green;
-  return *this;
-}
-LightCall &LightCall::set_green(float green) {
-  this->green_ = green;
-  return *this;
-}
-LightCall &LightCall::set_blue(optional blue) {
-  this->blue_ = blue;
-  return *this;
-}
-LightCall &LightCall::set_blue(float blue) {
-  this->blue_ = blue;
-  return *this;
-}
-LightCall &LightCall::set_white(optional white) {
-  this->white_ = white;
-  return *this;
-}
-LightCall &LightCall::set_white(float white) {
-  this->white_ = white;
-  return *this;
-}
-LightCall &LightCall::set_color_temperature(optional color_temperature) {
-  this->color_temperature_ = color_temperature;
-  return *this;
-}
-LightCall &LightCall::set_color_temperature(float color_temperature) {
-  this->color_temperature_ = color_temperature;
-  return *this;
-}
-LightCall &LightCall::set_cold_white(optional cold_white) {
-  this->cold_white_ = cold_white;
-  return *this;
-}
-LightCall &LightCall::set_cold_white(float cold_white) {
-  this->cold_white_ = cold_white;
-  return *this;
-}
-LightCall &LightCall::set_warm_white(optional warm_white) {
-  this->warm_white_ = warm_white;
-  return *this;
-}
-LightCall &LightCall::set_warm_white(float warm_white) {
-  this->warm_white_ = warm_white;
-  return *this;
-}
+IMPLEMENT_LIGHT_CALL_SETTER(state, bool, FLAG_HAS_STATE)
+IMPLEMENT_LIGHT_CALL_SETTER(transition_length, uint32_t, FLAG_HAS_TRANSITION)
+IMPLEMENT_LIGHT_CALL_SETTER(flash_length, uint32_t, FLAG_HAS_FLASH)
+IMPLEMENT_LIGHT_CALL_SETTER(brightness, float, FLAG_HAS_BRIGHTNESS)
+IMPLEMENT_LIGHT_CALL_SETTER(color_mode, ColorMode, FLAG_HAS_COLOR_MODE)
+IMPLEMENT_LIGHT_CALL_SETTER(color_brightness, float, FLAG_HAS_COLOR_BRIGHTNESS)
+IMPLEMENT_LIGHT_CALL_SETTER(red, float, FLAG_HAS_RED)
+IMPLEMENT_LIGHT_CALL_SETTER(green, float, FLAG_HAS_GREEN)
+IMPLEMENT_LIGHT_CALL_SETTER(blue, float, FLAG_HAS_BLUE)
+IMPLEMENT_LIGHT_CALL_SETTER(white, float, FLAG_HAS_WHITE)
+IMPLEMENT_LIGHT_CALL_SETTER(color_temperature, float, FLAG_HAS_COLOR_TEMPERATURE)
+IMPLEMENT_LIGHT_CALL_SETTER(cold_white, float, FLAG_HAS_COLD_WHITE)
+IMPLEMENT_LIGHT_CALL_SETTER(warm_white, float, FLAG_HAS_WARM_WHITE)
 LightCall &LightCall::set_effect(optional effect) {
   if (effect.has_value())
     this->set_effect(*effect);
@@ -660,18 +593,22 @@ LightCall &LightCall::set_effect(optional effect) {
 }
 LightCall &LightCall::set_effect(uint32_t effect_number) {
   this->effect_ = effect_number;
+  this->set_flag_(FLAG_HAS_EFFECT, true);
   return *this;
 }
 LightCall &LightCall::set_effect(optional effect_number) {
-  this->effect_ = effect_number;
+  if (effect_number.has_value()) {
+    this->effect_ = effect_number.value();
+  }
+  this->set_flag_(FLAG_HAS_EFFECT, effect_number.has_value());
   return *this;
 }
 LightCall &LightCall::set_publish(bool publish) {
-  this->publish_ = publish;
+  this->set_flag_(FLAG_PUBLISH, publish);
   return *this;
 }
 LightCall &LightCall::set_save(bool save) {
-  this->save_ = save;
+  this->set_flag_(FLAG_SAVE, save);
   return *this;
 }
 LightCall &LightCall::set_rgb(float red, float green, float blue) {
diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h
index bca2ac7b07..7e04e1a767 100644
--- a/esphome/components/light/light_call.h
+++ b/esphome/components/light/light_call.h
@@ -1,6 +1,5 @@
 #pragma once
 
-#include "esphome/core/optional.h"
 #include "light_color_values.h"
 #include 
 
@@ -10,6 +9,11 @@ namespace light {
 class LightState;
 
 /** This class represents a requested change in a light state.
+ *
+ * Light state changes are tracked using a bitfield flags_ to minimize memory usage.
+ * Each possible light property has a flag indicating whether it has been set.
+ * This design keeps LightCall at ~56 bytes to minimize heap fragmentation on
+ * ESP8266 and other memory-constrained devices.
  */
 class LightCall {
  public:
@@ -131,6 +135,19 @@ class LightCall {
   /// Set whether this light call should trigger a save state to recover them at startup..
   LightCall &set_save(bool save);
 
+  // Getter methods to check if values are set
+  bool has_state() const { return (flags_ & FLAG_HAS_STATE) != 0; }
+  bool has_brightness() const { return (flags_ & FLAG_HAS_BRIGHTNESS) != 0; }
+  bool has_color_brightness() const { return (flags_ & FLAG_HAS_COLOR_BRIGHTNESS) != 0; }
+  bool has_red() const { return (flags_ & FLAG_HAS_RED) != 0; }
+  bool has_green() const { return (flags_ & FLAG_HAS_GREEN) != 0; }
+  bool has_blue() const { return (flags_ & FLAG_HAS_BLUE) != 0; }
+  bool has_white() const { return (flags_ & FLAG_HAS_WHITE) != 0; }
+  bool has_color_temperature() const { return (flags_ & FLAG_HAS_COLOR_TEMPERATURE) != 0; }
+  bool has_cold_white() const { return (flags_ & FLAG_HAS_COLD_WHITE) != 0; }
+  bool has_warm_white() const { return (flags_ & FLAG_HAS_WARM_WHITE) != 0; }
+  bool has_color_mode() const { return (flags_ & FLAG_HAS_COLOR_MODE) != 0; }
+
   /** Set the RGB color of the light by RGB values.
    *
    * Please note that this only changes the color of the light, not the brightness.
@@ -170,27 +187,62 @@ class LightCall {
   /// Some color modes also can be set using non-native parameters, transform those calls.
   void transform_parameters_();
 
-  bool has_transition_() { return this->transition_length_.has_value(); }
-  bool has_flash_() { return this->flash_length_.has_value(); }
-  bool has_effect_() { return this->effect_.has_value(); }
+  // Bitfield flags - each flag indicates whether a corresponding value has been set.
+  enum FieldFlags : uint16_t {
+    FLAG_HAS_STATE = 1 << 0,
+    FLAG_HAS_TRANSITION = 1 << 1,
+    FLAG_HAS_FLASH = 1 << 2,
+    FLAG_HAS_EFFECT = 1 << 3,
+    FLAG_HAS_BRIGHTNESS = 1 << 4,
+    FLAG_HAS_COLOR_BRIGHTNESS = 1 << 5,
+    FLAG_HAS_RED = 1 << 6,
+    FLAG_HAS_GREEN = 1 << 7,
+    FLAG_HAS_BLUE = 1 << 8,
+    FLAG_HAS_WHITE = 1 << 9,
+    FLAG_HAS_COLOR_TEMPERATURE = 1 << 10,
+    FLAG_HAS_COLD_WHITE = 1 << 11,
+    FLAG_HAS_WARM_WHITE = 1 << 12,
+    FLAG_HAS_COLOR_MODE = 1 << 13,
+    FLAG_PUBLISH = 1 << 14,
+    FLAG_SAVE = 1 << 15,
+  };
+
+  bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; }
+  bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; }
+  bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; }
+  bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; }
+  bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; }
+
+  // Helper to set flag
+  void set_flag_(FieldFlags flag, bool value) {
+    if (value) {
+      this->flags_ |= flag;
+    } else {
+      this->flags_ &= ~flag;
+    }
+  }
 
   LightState *parent_;
-  optional state_;
-  optional transition_length_;
-  optional flash_length_;
-  optional color_mode_;
-  optional brightness_;
-  optional color_brightness_;
-  optional red_;
-  optional green_;
-  optional blue_;
-  optional white_;
-  optional color_temperature_;
-  optional cold_white_;
-  optional warm_white_;
-  optional effect_;
-  bool publish_{true};
-  bool save_{true};
+
+  // Light state values - use flags_ to check if a value has been set.
+  // Group 4-byte aligned members first
+  uint32_t transition_length_;
+  uint32_t flash_length_;
+  uint32_t effect_;
+  float brightness_;
+  float color_brightness_;
+  float red_;
+  float green_;
+  float blue_;
+  float white_;
+  float color_temperature_;
+  float cold_white_;
+  float warm_white_;
+
+  // Smaller members at the end for better packing
+  uint16_t flags_{FLAG_PUBLISH | FLAG_SAVE};  // Tracks which values are set
+  ColorMode color_mode_;
+  bool state_;
 };
 
 }  // namespace light
diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h
index d8eaa6ae24..5653a8d2a5 100644
--- a/esphome/components/light/light_color_values.h
+++ b/esphome/components/light/light_color_values.h
@@ -46,8 +46,7 @@ class LightColorValues {
  public:
   /// Construct the LightColorValues with all attributes enabled, but state set to off.
   LightColorValues()
-      : color_mode_(ColorMode::UNKNOWN),
-        state_(0.0f),
+      : state_(0.0f),
         brightness_(1.0f),
         color_brightness_(1.0f),
         red_(1.0f),
@@ -56,7 +55,8 @@ class LightColorValues {
         white_(1.0f),
         color_temperature_{0.0f},
         cold_white_{1.0f},
-        warm_white_{1.0f} {}
+        warm_white_{1.0f},
+        color_mode_(ColorMode::UNKNOWN) {}
 
   LightColorValues(ColorMode color_mode, float state, float brightness, float color_brightness, float red, float green,
                    float blue, float white, float color_temperature, float cold_white, float warm_white) {
@@ -292,7 +292,6 @@ class LightColorValues {
   void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); }
 
  protected:
-  ColorMode color_mode_;
   float state_;  ///< ON / OFF, float for transition
   float brightness_;
   float color_brightness_;
@@ -303,6 +302,7 @@ class LightColorValues {
   float color_temperature_;  ///< Color Temperature in Mired
   float cold_white_;
   float warm_white_;
+  ColorMode color_mode_;
 };
 
 }  // namespace light
diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h
index f21fb8a06e..72cb99223e 100644
--- a/esphome/components/light/light_state.h
+++ b/esphome/components/light/light_state.h
@@ -31,9 +31,7 @@ enum LightRestoreMode : uint8_t {
 struct LightStateRTCState {
   LightStateRTCState(ColorMode color_mode, bool state, float brightness, float color_brightness, float red, float green,
                      float blue, float white, float color_temp, float cold_white, float warm_white)
-      : color_mode(color_mode),
-        state(state),
-        brightness(brightness),
+      : brightness(brightness),
         color_brightness(color_brightness),
         red(red),
         green(green),
@@ -41,10 +39,12 @@ struct LightStateRTCState {
         white(white),
         color_temp(color_temp),
         cold_white(cold_white),
-        warm_white(warm_white) {}
+        warm_white(warm_white),
+        effect(0),
+        color_mode(color_mode),
+        state(state) {}
   LightStateRTCState() = default;
-  ColorMode color_mode{ColorMode::UNKNOWN};
-  bool state{false};
+  // Group 4-byte aligned members first
   float brightness{1.0f};
   float color_brightness{1.0f};
   float red{1.0f};
@@ -55,6 +55,9 @@ struct LightStateRTCState {
   float cold_white{1.0f};
   float warm_white{1.0f};
   uint32_t effect{0};
+  // Group smaller members at the end
+  ColorMode color_mode{ColorMode::UNKNOWN};
+  bool state{false};
 };
 
 /** This class represents the communication layer between the front-end MQTT layer and the
@@ -216,6 +219,8 @@ class LightState : public EntityBase, public Component {
   std::unique_ptr transformer_{nullptr};
   /// List of effects for this light.
   std::vector effects_;
+  /// Object used to store the persisted values of the light.
+  ESPPreferenceObject rtc_;
   /// Value for storing the index of the currently active effect. 0 if no effect is active
   uint32_t active_effect_index_{};
   /// Default transition length for all transitions in ms.
@@ -224,15 +229,11 @@ class LightState : public EntityBase, public Component {
   uint32_t flash_transition_length_{};
   /// Gamma correction factor for the light.
   float gamma_correct_{};
-
   /// Whether the light value should be written in the next cycle.
   bool next_write_{true};
   // for effects, true if a transformer (transition) is active.
   bool is_transformer_active_ = false;
 
-  /// Object used to store the persisted values of the light.
-  ESPPreferenceObject rtc_;
-
   /** Callback to call when new values for the frontend are available.
    *
    * "Remote values" are light color values that are reported to the frontend and have a lower
diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h
index a557bd39b1..8d49acff97 100644
--- a/esphome/components/light/transformers.h
+++ b/esphome/components/light/transformers.h
@@ -59,9 +59,9 @@ class LightTransitionTransformer : public LightTransformer {
   // transition from 0 to 1 on x = [0, 1]
   static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); }
 
-  bool changing_color_mode_{false};
   LightColorValues end_values_{};
   LightColorValues intermediate_values_{};
+  bool changing_color_mode_{false};
 };
 
 class LightFlashTransformer : public LightTransformer {
@@ -117,8 +117,8 @@ class LightFlashTransformer : public LightTransformer {
 
  protected:
   LightState &state_;
-  uint32_t transition_length_;
   std::unique_ptr transformer_{nullptr};
+  uint32_t transition_length_;
   bool begun_lightstate_restore_;
 };
 
diff --git a/tests/integration/fixtures/light_calls.yaml b/tests/integration/fixtures/light_calls.yaml
new file mode 100644
index 0000000000..d692a11765
--- /dev/null
+++ b/tests/integration/fixtures/light_calls.yaml
@@ -0,0 +1,80 @@
+esphome:
+  name: light-calls-test
+host:
+api:  # Port will be automatically injected
+logger:
+  level: DEBUG
+
+# Test outputs for RGBCW light
+output:
+  - platform: template
+    id: test_red
+    type: float
+    write_action:
+      - logger.log:
+          format: "Red output: %.2f"
+          args: [state]
+  - platform: template
+    id: test_green
+    type: float
+    write_action:
+      - logger.log:
+          format: "Green output: %.2f"
+          args: [state]
+  - platform: template
+    id: test_blue
+    type: float
+    write_action:
+      - logger.log:
+          format: "Blue output: %.2f"
+          args: [state]
+  - platform: template
+    id: test_cold_white
+    type: float
+    write_action:
+      - logger.log:
+          format: "Cold white output: %.2f"
+          args: [state]
+  - platform: template
+    id: test_warm_white
+    type: float
+    write_action:
+      - logger.log:
+          format: "Warm white output: %.2f"
+          args: [state]
+
+light:
+  - platform: rgbww
+    name: "Test RGBCW Light"
+    id: test_light
+    red: test_red
+    green: test_green
+    blue: test_blue
+    cold_white: test_cold_white
+    warm_white: test_warm_white
+    cold_white_color_temperature: 6536 K
+    warm_white_color_temperature: 2000 K
+    constant_brightness: true
+    effects:
+      - random:
+          name: "Random Effect"
+          transition_length: 100ms
+          update_interval: 200ms
+      - strobe:
+          name: "Strobe Effect"
+      - pulse:
+          name: "Pulse Effect"
+          transition_length: 100ms
+
+  # Additional lights to test memory with multiple instances
+  - platform: rgb
+    name: "Test RGB Light"
+    id: test_rgb_light
+    red: test_red
+    green: test_green
+    blue: test_blue
+
+  - platform: binary
+    name: "Test Binary Light"
+    id: test_binary_light
+    output: test_red
diff --git a/tests/integration/test_light_calls.py b/tests/integration/test_light_calls.py
new file mode 100644
index 0000000000..8ecb77fb99
--- /dev/null
+++ b/tests/integration/test_light_calls.py
@@ -0,0 +1,189 @@
+"""Integration test for all light call combinations.
+
+Tests that LightCall handles all possible light operations correctly
+including RGB, color temperature, effects, transitions, and flash.
+"""
+
+import asyncio
+from typing import Any
+
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_light_calls(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test all possible LightCall operations and combinations."""
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Track state changes with futures
+        state_futures: dict[int, asyncio.Future[Any]] = {}
+        states: dict[int, Any] = {}
+
+        def on_state(state: Any) -> None:
+            states[state.key] = state
+            if state.key in state_futures and not state_futures[state.key].done():
+                state_futures[state.key].set_result(state)
+
+        client.subscribe_states(on_state)
+
+        # Get the light entities
+        entities = await client.list_entities_services()
+        lights = [e for e in entities[0] if e.object_id.startswith("test_")]
+        assert len(lights) >= 2  # Should have RGBCW and RGB lights
+
+        rgbcw_light = next(light for light in lights if "RGBCW" in light.name)
+        rgb_light = next(light for light in lights if "RGB Light" in light.name)
+
+        async def wait_for_state_change(key: int, timeout: float = 1.0) -> Any:
+            """Wait for a state change for the given entity key."""
+            loop = asyncio.get_event_loop()
+            state_futures[key] = loop.create_future()
+            try:
+                return await asyncio.wait_for(state_futures[key], timeout)
+            finally:
+                state_futures.pop(key, None)
+
+        # Test all individual parameters first
+
+        # Test 1: state only
+        client.light_command(key=rgbcw_light.key, state=True)
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.state is True
+
+        # Test 2: brightness only
+        client.light_command(key=rgbcw_light.key, brightness=0.5)
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.brightness == pytest.approx(0.5)
+
+        # Test 3: color_brightness only
+        client.light_command(key=rgbcw_light.key, color_brightness=0.8)
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.color_brightness == pytest.approx(0.8)
+
+        # Test 4-7: RGB values must be set together via rgb parameter
+        client.light_command(key=rgbcw_light.key, rgb=(0.7, 0.3, 0.9))
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.red == pytest.approx(0.7, abs=0.1)
+        assert state.green == pytest.approx(0.3, abs=0.1)
+        assert state.blue == pytest.approx(0.9, abs=0.1)
+
+        # Test 7: white value
+        client.light_command(key=rgbcw_light.key, white=0.6)
+        state = await wait_for_state_change(rgbcw_light.key)
+        # White might need more tolerance or might not be directly settable
+        if hasattr(state, "white"):
+            assert state.white == pytest.approx(0.6, abs=0.1)
+
+        # Test 8: color_temperature only
+        client.light_command(key=rgbcw_light.key, color_temperature=300)
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.color_temperature == pytest.approx(300)
+
+        # Test 9: cold_white only
+        client.light_command(key=rgbcw_light.key, cold_white=0.8)
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.cold_white == pytest.approx(0.8)
+
+        # Test 10: warm_white only
+        client.light_command(key=rgbcw_light.key, warm_white=0.2)
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.warm_white == pytest.approx(0.2)
+
+        # Test 11: transition_length with state change
+        client.light_command(key=rgbcw_light.key, state=False, transition_length=0.1)
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.state is False
+
+        # Test 12: flash_length
+        client.light_command(key=rgbcw_light.key, state=True, flash_length=0.2)
+        state = await wait_for_state_change(rgbcw_light.key)
+        # Flash starts
+        assert state.state is True
+        # Wait for flash to end
+        state = await wait_for_state_change(rgbcw_light.key)
+
+        # Test 13: effect only
+        # First ensure light is on
+        client.light_command(key=rgbcw_light.key, state=True)
+        state = await wait_for_state_change(rgbcw_light.key)
+        # Now set effect
+        client.light_command(key=rgbcw_light.key, effect="Random Effect")
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.effect == "Random Effect"
+
+        # Test 14: stop effect
+        client.light_command(key=rgbcw_light.key, effect="None")
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.effect == "None"
+
+        # Test 15: color_mode parameter
+        client.light_command(
+            key=rgbcw_light.key, state=True, color_mode=5
+        )  # COLD_WARM_WHITE
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.state is True
+
+        # Now test common combinations
+
+        # Test 16: RGB combination (set_rgb) - RGB values get normalized
+        client.light_command(key=rgbcw_light.key, rgb=(1.0, 0.0, 0.5))
+        state = await wait_for_state_change(rgbcw_light.key)
+        # RGB values get normalized - in this case red is already 1.0
+        assert state.red == pytest.approx(1.0, abs=0.1)
+        assert state.green == pytest.approx(0.0, abs=0.1)
+        assert state.blue == pytest.approx(0.5, abs=0.1)
+
+        # Test 17: Multiple RGB changes to test transitions
+        client.light_command(key=rgbcw_light.key, rgb=(0.2, 0.8, 0.4))
+        state = await wait_for_state_change(rgbcw_light.key)
+        # RGB values get normalized so green (highest) becomes 1.0
+        # Expected: (0.2/0.8, 0.8/0.8, 0.4/0.8) = (0.25, 1.0, 0.5)
+        assert state.red == pytest.approx(0.25, abs=0.01)
+        assert state.green == pytest.approx(1.0, abs=0.01)
+        assert state.blue == pytest.approx(0.5, abs=0.01)
+
+        # Test 18: State + brightness + transition
+        client.light_command(
+            key=rgbcw_light.key, state=True, brightness=0.7, transition_length=0.1
+        )
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.state is True
+        assert state.brightness == pytest.approx(0.7)
+
+        # Test 19: RGB + brightness + color_brightness
+        client.light_command(
+            key=rgb_light.key,
+            state=True,
+            brightness=0.8,
+            color_brightness=0.9,
+            rgb=(0.2, 0.4, 0.6),
+        )
+        state = await wait_for_state_change(rgb_light.key)
+        assert state.state is True
+        assert state.brightness == pytest.approx(0.8)
+
+        # Test 20: Color temp + cold/warm white
+        client.light_command(
+            key=rgbcw_light.key, color_temperature=250, cold_white=0.7, warm_white=0.3
+        )
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.color_temperature == pytest.approx(250)
+
+        # Test 21: Turn RGB light off
+        client.light_command(key=rgb_light.key, state=False)
+        state = await wait_for_state_change(rgb_light.key)
+        assert state.state is False
+
+        # Final cleanup - turn all lights off
+        for light in lights:
+            client.light_command(
+                key=light.key,
+                state=False,
+            )
+            state = await wait_for_state_change(light.key)
+            assert state.state is False

From 053feb5e3b4a288bae55920f998b06af5094ac60 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 15:22:40 -0500
Subject: [PATCH 262/293] Optimize entity icon memory usage with
 USE_ENTITY_ICON flag (#9337)

---
 esphome/core/defines.h                      |  1 +
 esphome/core/entity_base.cpp                | 12 ++-
 esphome/core/entity_base.h                  |  2 +
 esphome/core/entity_helpers.py              |  3 +
 tests/integration/fixtures/entity_icon.yaml | 78 +++++++++++++++++
 tests/integration/test_entity_icon.py       | 97 +++++++++++++++++++++
 6 files changed, 192 insertions(+), 1 deletion(-)
 create mode 100644 tests/integration/fixtures/entity_icon.yaml
 create mode 100644 tests/integration/test_entity_icon.py

diff --git a/esphome/core/defines.h b/esphome/core/defines.h
index 4115b97391..d73009436b 100644
--- a/esphome/core/defines.h
+++ b/esphome/core/defines.h
@@ -33,6 +33,7 @@
 #define USE_DEEP_SLEEP
 #define USE_DEVICES
 #define USE_DISPLAY
+#define USE_ENTITY_ICON
 #define USE_ESP32_IMPROV_STATE_CALLBACK
 #define USE_EVENT
 #define USE_FAN
diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp
index 6afd02ff65..2ea9c77a3e 100644
--- a/esphome/core/entity_base.cpp
+++ b/esphome/core/entity_base.cpp
@@ -27,12 +27,22 @@ void EntityBase::set_name(const char *name) {
 
 // Entity Icon
 std::string EntityBase::get_icon() const {
+#ifdef USE_ENTITY_ICON
   if (this->icon_c_str_ == nullptr) {
     return "";
   }
   return this->icon_c_str_;
+#else
+  return "";
+#endif
+}
+void EntityBase::set_icon(const char *icon) {
+#ifdef USE_ENTITY_ICON
+  this->icon_c_str_ = icon;
+#else
+  // No-op when USE_ENTITY_ICON is not defined
+#endif
 }
-void EntityBase::set_icon(const char *icon) { this->icon_c_str_ = icon; }
 
 // Entity Object ID
 std::string EntityBase::get_object_id() const {
diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h
index 4819b66108..00b1264ed0 100644
--- a/esphome/core/entity_base.h
+++ b/esphome/core/entity_base.h
@@ -80,7 +80,9 @@ class EntityBase {
 
   StringRef name_;
   const char *object_id_c_str_{nullptr};
+#ifdef USE_ENTITY_ICON
   const char *icon_c_str_{nullptr};
+#endif
   uint32_t object_id_hash_{};
 #ifdef USE_DEVICES
   Device *device_{};
diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py
index 2442fbca4b..a3244856a2 100644
--- a/esphome/core/entity_helpers.py
+++ b/esphome/core/entity_helpers.py
@@ -1,6 +1,7 @@
 from collections.abc import Callable
 import logging
 
+import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_DEVICE_ID,
@@ -108,6 +109,8 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
     if CONF_INTERNAL in config:
         add(var.set_internal(config[CONF_INTERNAL]))
     if CONF_ICON in config:
+        # Add USE_ENTITY_ICON define when icons are used
+        cg.add_define("USE_ENTITY_ICON")
         add(var.set_icon(config[CONF_ICON]))
     if CONF_ENTITY_CATEGORY in config:
         add(var.set_entity_category(config[CONF_ENTITY_CATEGORY]))
diff --git a/tests/integration/fixtures/entity_icon.yaml b/tests/integration/fixtures/entity_icon.yaml
new file mode 100644
index 0000000000..2ce633fe2c
--- /dev/null
+++ b/tests/integration/fixtures/entity_icon.yaml
@@ -0,0 +1,78 @@
+esphome:
+  name: icon-test
+
+host:
+
+api:
+
+logger:
+
+# Test entities with custom icons
+sensor:
+  - platform: template
+    name: "Sensor With Icon"
+    icon: "mdi:temperature-celsius"
+    unit_of_measurement: "°C"
+    update_interval: 1s
+    lambda: |-
+      return 25.5;
+
+  - platform: template
+    name: "Sensor Without Icon"
+    unit_of_measurement: "%"
+    update_interval: 1s
+    lambda: |-
+      return 50.0;
+
+binary_sensor:
+  - platform: template
+    name: "Binary Sensor With Icon"
+    icon: "mdi:motion-sensor"
+    lambda: |-
+      return true;
+
+  - platform: template
+    name: "Binary Sensor Without Icon"
+    lambda: |-
+      return false;
+
+text_sensor:
+  - platform: template
+    name: "Text Sensor With Icon"
+    icon: "mdi:text-box"
+    lambda: |-
+      return {"Hello Icons"};
+
+switch:
+  - platform: template
+    name: "Switch With Icon"
+    icon: "mdi:toggle-switch"
+    optimistic: true
+
+button:
+  - platform: template
+    name: "Button With Icon"
+    icon: "mdi:gesture-tap-button"
+    on_press:
+      - logger.log: "Button with icon pressed"
+
+number:
+  - platform: template
+    name: "Number With Icon"
+    icon: "mdi:numeric"
+    initial_value: 42
+    min_value: 0
+    max_value: 100
+    step: 1
+    optimistic: true
+
+select:
+  - platform: template
+    name: "Select With Icon"
+    icon: "mdi:format-list-bulleted"
+    options:
+      - "Option A"
+      - "Option B"
+      - "Option C"
+    initial_option: "Option A"
+    optimistic: true
diff --git a/tests/integration/test_entity_icon.py b/tests/integration/test_entity_icon.py
new file mode 100644
index 0000000000..56e266b486
--- /dev/null
+++ b/tests/integration/test_entity_icon.py
@@ -0,0 +1,97 @@
+"""Integration test for entity icons with USE_ENTITY_ICON feature."""
+
+from __future__ import annotations
+
+import asyncio
+
+from aioesphomeapi import EntityState
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_entity_icon(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that entities with custom icons work correctly with USE_ENTITY_ICON."""
+    # Write, compile and run the ESPHome device, then connect to API
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Get all entities
+        entities = await client.list_entities_services()
+
+        # Create a map of entity names to entity info
+        entity_map = {entity.name: entity for entity in entities[0]}
+
+        # Test entities with icons
+        icon_test_cases = [
+            # (entity_name, expected_icon)
+            ("Sensor With Icon", "mdi:temperature-celsius"),
+            ("Binary Sensor With Icon", "mdi:motion-sensor"),
+            ("Text Sensor With Icon", "mdi:text-box"),
+            ("Switch With Icon", "mdi:toggle-switch"),
+            ("Button With Icon", "mdi:gesture-tap-button"),
+            ("Number With Icon", "mdi:numeric"),
+            ("Select With Icon", "mdi:format-list-bulleted"),
+        ]
+
+        # Test entities without icons (should have empty string)
+        no_icon_test_cases = [
+            "Sensor Without Icon",
+            "Binary Sensor Without Icon",
+        ]
+
+        # Verify entities with icons
+        for entity_name, expected_icon in icon_test_cases:
+            assert entity_name in entity_map, (
+                f"Entity '{entity_name}' not found in API response"
+            )
+            entity = entity_map[entity_name]
+
+            # Check icon field
+            assert hasattr(entity, "icon"), (
+                f"{entity_name}: Entity should have icon attribute"
+            )
+            assert entity.icon == expected_icon, (
+                f"{entity_name}: icon mismatch - "
+                f"expected '{expected_icon}', got '{entity.icon}'"
+            )
+
+        # Verify entities without icons
+        for entity_name in no_icon_test_cases:
+            assert entity_name in entity_map, (
+                f"Entity '{entity_name}' not found in API response"
+            )
+            entity = entity_map[entity_name]
+
+            # Check icon field is empty
+            assert hasattr(entity, "icon"), (
+                f"{entity_name}: Entity should have icon attribute"
+            )
+            assert entity.icon == "", (
+                f"{entity_name}: icon should be empty string for entities without icons, "
+                f"got '{entity.icon}'"
+            )
+
+        # Subscribe to states to ensure everything works normally
+        states: dict[int, EntityState] = {}
+        state_received = asyncio.Event()
+
+        def on_state(state: EntityState) -> None:
+            states[state.key] = state
+            state_received.set()
+
+        client.subscribe_states(on_state)
+
+        # Wait for states
+        try:
+            await asyncio.wait_for(state_received.wait(), timeout=5.0)
+        except asyncio.TimeoutError:
+            pytest.fail("No states received within 5 seconds")
+
+        # Verify we received states
+        assert len(states) > 0, (
+            "No states received - entities may not be working correctly"
+        )

From e2de6ee29d7a7f29df04fd0f869f212cbf33a01d Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 15:28:14 -0500
Subject: [PATCH 263/293] Reduce core RAM usage by 40 bytes with static
 initialization optimizations (#9340)

---
 esphome/core/component.cpp | 46 +++++++++++++++++++-------------------
 esphome/core/helpers.cpp   | 30 +++++++++++++++++++------
 2 files changed, 46 insertions(+), 30 deletions(-)

diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp
index 9ef30081aa..9d863e56cd 100644
--- a/esphome/core/component.cpp
+++ b/esphome/core/component.cpp
@@ -26,17 +26,17 @@ static const char *const TAG = "component";
 // 1. Components are never destroyed in ESPHome
 // 2. Failed components remain failed (no recovery mechanism)
 // 3. Memory usage is minimal (only failures with custom messages are stored)
-static std::unique_ptr>> &get_component_error_messages() {
-  static std::unique_ptr>> instance;
-  return instance;
-}
 
+// Using namespace-scope static to avoid guard variables (saves 16 bytes total)
+// This is safe because ESPHome is single-threaded during initialization
+namespace {
+// Error messages for failed components
+// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
+std::unique_ptr>> component_error_messages;
 // Setup priority overrides - freed after setup completes
-// Typically < 5 entries, lazy allocated
-static std::unique_ptr>> &get_setup_priority_overrides() {
-  static std::unique_ptr>> instance;
-  return instance;
-}
+// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
+std::unique_ptr>> setup_priority_overrides;
+}  // namespace
 
 namespace setup_priority {
 
@@ -130,8 +130,8 @@ void Component::call_dump_config() {
   if (this->is_failed()) {
     // Look up error message from global vector
     const char *error_msg = "unspecified";
-    if (get_component_error_messages()) {
-      for (const auto &pair : *get_component_error_messages()) {
+    if (component_error_messages) {
+      for (const auto &pair : *component_error_messages) {
         if (pair.first == this) {
           error_msg = pair.second;
           break;
@@ -285,18 +285,18 @@ void Component::status_set_error(const char *message) {
   ESP_LOGE(TAG, "Component %s set Error flag: %s", this->get_component_source(), message);
   if (strcmp(message, "unspecified") != 0) {
     // Lazy allocate the error messages vector if needed
-    if (!get_component_error_messages()) {
-      get_component_error_messages() = std::make_unique>>();
+    if (!component_error_messages) {
+      component_error_messages = std::make_unique>>();
     }
     // Check if this component already has an error message
-    for (auto &pair : *get_component_error_messages()) {
+    for (auto &pair : *component_error_messages) {
       if (pair.first == this) {
         pair.second = message;
         return;
       }
     }
     // Add new error message
-    get_component_error_messages()->emplace_back(this, message);
+    component_error_messages->emplace_back(this, message);
   }
 }
 void Component::status_clear_warning() {
@@ -322,9 +322,9 @@ void Component::status_momentary_error(const std::string &name, uint32_t length)
 void Component::dump_config() {}
 float Component::get_actual_setup_priority() const {
   // Check if there's an override in the global vector
-  if (get_setup_priority_overrides()) {
+  if (setup_priority_overrides) {
     // Linear search is fine for small n (typically < 5 overrides)
-    for (const auto &pair : *get_setup_priority_overrides()) {
+    for (const auto &pair : *setup_priority_overrides) {
       if (pair.first == this) {
         return pair.second;
       }
@@ -334,14 +334,14 @@ float Component::get_actual_setup_priority() const {
 }
 void Component::set_setup_priority(float priority) {
   // Lazy allocate the vector if needed
-  if (!get_setup_priority_overrides()) {
-    get_setup_priority_overrides() = std::make_unique>>();
+  if (!setup_priority_overrides) {
+    setup_priority_overrides = std::make_unique>>();
     // Reserve some space to avoid reallocations (most configs have < 10 overrides)
-    get_setup_priority_overrides()->reserve(10);
+    setup_priority_overrides->reserve(10);
   }
 
   // Check if this component already has an override
-  for (auto &pair : *get_setup_priority_overrides()) {
+  for (auto &pair : *setup_priority_overrides) {
     if (pair.first == this) {
       pair.second = priority;
       return;
@@ -349,7 +349,7 @@ void Component::set_setup_priority(float priority) {
   }
 
   // Add new override
-  get_setup_priority_overrides()->emplace_back(this, priority);
+  setup_priority_overrides->emplace_back(this, priority);
 }
 
 bool Component::has_overridden_loop() const {
@@ -414,7 +414,7 @@ WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {}
 
 void clear_setup_priority_overrides() {
   // Free the setup priority map completely
-  get_setup_priority_overrides().reset();
+  setup_priority_overrides.reset();
 }
 
 }  // namespace esphome
diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp
index 72722169d4..22b74e11fa 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -360,9 +360,22 @@ int8_t step_to_accuracy_decimals(float step) {
   return str.length() - dot_pos - 1;
 }
 
-static const std::string BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
-                                        "abcdefghijklmnopqrstuvwxyz"
-                                        "0123456789+/";
+// Use C-style string constant to store in ROM instead of RAM (saves 24 bytes)
+static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                                            "abcdefghijklmnopqrstuvwxyz"
+                                            "0123456789+/";
+
+// Helper function to find the index of a base64 character in the lookup table.
+// Returns the character's position (0-63) if found, or 0 if not found.
+// NOTE: This returns 0 for both 'A' (valid base64 char at index 0) and invalid characters.
+// This is safe because is_base64() is ALWAYS checked before calling this function,
+// preventing invalid characters from ever reaching here. The base64_decode function
+// stops processing at the first invalid character due to the is_base64() check in its
+// while loop condition, making this edge case harmless in practice.
+static inline uint8_t base64_find_char(char c) {
+  const char *pos = strchr(BASE64_CHARS, c);
+  return pos ? (pos - BASE64_CHARS) : 0;
+}
 
 static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/')); }
 
@@ -384,7 +397,7 @@ std::string base64_encode(const uint8_t *buf, size_t buf_len) {
       char_array_4[3] = char_array_3[2] & 0x3f;
 
       for (i = 0; (i < 4); i++)
-        ret += BASE64_CHARS[char_array_4[i]];
+        ret += BASE64_CHARS[static_cast(char_array_4[i])];
       i = 0;
     }
   }
@@ -399,7 +412,7 @@ std::string base64_encode(const uint8_t *buf, size_t buf_len) {
     char_array_4[3] = char_array_3[2] & 0x3f;
 
     for (j = 0; (j < i + 1); j++)
-      ret += BASE64_CHARS[char_array_4[j]];
+      ret += BASE64_CHARS[static_cast(char_array_4[j])];
 
     while ((i++ < 3))
       ret += '=';
@@ -426,12 +439,15 @@ std::vector base64_decode(const std::string &encoded_string) {
   uint8_t char_array_4[4], char_array_3[3];
   std::vector ret;
 
+  // SAFETY: The loop condition checks is_base64() before processing each character.
+  // This ensures base64_find_char() is only called on valid base64 characters,
+  // preventing the edge case where invalid chars would return 0 (same as 'A').
   while (in_len-- && (encoded_string[in] != '=') && is_base64(encoded_string[in])) {
     char_array_4[i++] = encoded_string[in];
     in++;
     if (i == 4) {
       for (i = 0; i < 4; i++)
-        char_array_4[i] = BASE64_CHARS.find(char_array_4[i]);
+        char_array_4[i] = base64_find_char(char_array_4[i]);
 
       char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
       char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
@@ -448,7 +464,7 @@ std::vector base64_decode(const std::string &encoded_string) {
       char_array_4[j] = 0;
 
     for (j = 0; j < 4; j++)
-      char_array_4[j] = BASE64_CHARS.find(char_array_4[j]);
+      char_array_4[j] = base64_find_char(char_array_4[j]);
 
     char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
     char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);

From 29747fc7307f2ef7105255b6aeb9086de5d8f920 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 15:35:11 -0500
Subject: [PATCH 264/293] Fix flaky test_api_conditional_memory by disabling
 API batch delay (#9360)

---
 tests/integration/fixtures/api_conditional_memory.yaml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tests/integration/fixtures/api_conditional_memory.yaml b/tests/integration/fixtures/api_conditional_memory.yaml
index 4bbba5084b..49412c3bfe 100644
--- a/tests/integration/fixtures/api_conditional_memory.yaml
+++ b/tests/integration/fixtures/api_conditional_memory.yaml
@@ -2,6 +2,7 @@ esphome:
   name: api-conditional-memory-test
 host:
 api:
+  batch_delay: 0ms
   actions:
     - action: test_simple_service
       then:

From 832a7872719037e420b34046bab728d0b9515695 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 15:35:27 -0500
Subject: [PATCH 265/293] Fix format specifier warnings in QuantileFilter
 logging (#9364)

---
 esphome/components/sensor/filter.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp
index ce23c1f800..dd8635f0c0 100644
--- a/esphome/components/sensor/filter.cpp
+++ b/esphome/components/sensor/filter.cpp
@@ -118,7 +118,7 @@ optional QuantileFilter::new_value(float value) {
       size_t queue_size = quantile_queue.size();
       if (queue_size) {
         size_t position = ceilf(queue_size * this->quantile_) - 1;
-        ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %d/%d", this, position + 1, queue_size);
+        ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %zu/%zu", this, position + 1, queue_size);
         result = quantile_queue[position];
       }
     }

From 90fb3680d47333c7f8278439bb3867a330499c39 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 15:36:36 -0500
Subject: [PATCH 266/293] Optimize logger performance by eliminating redundant
 strlen calls (#9369)

---
 esphome/components/logger/logger.h         | 2 +-
 esphome/components/logger/logger_esp32.cpp | 4 +++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h
index 38faf73d84..e376d9fbf5 100644
--- a/esphome/components/logger/logger.h
+++ b/esphome/components/logger/logger.h
@@ -355,7 +355,7 @@ class Logger : public Component {
   }
 
   inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) {
-    static const uint16_t RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR);
+    static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
     this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size);
   }
 
diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp
index 41445fa3b4..2fde0f7d49 100644
--- a/esphome/components/logger/logger_esp32.cpp
+++ b/esphome/components/logger/logger_esp32.cpp
@@ -184,7 +184,9 @@ void HOT Logger::write_msg_(const char *msg) {
   ) {
     puts(msg);
   } else {
-    uart_write_bytes(this->uart_num_, msg, strlen(msg));
+    // Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen
+    size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg);
+    uart_write_bytes(this->uart_num_, msg, len);
     uart_write_bytes(this->uart_num_, "\n", 1);
   }
 }

From e58c793da2cdc5f5ca083606f1d1863b80cba11b Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 15:38:41 -0500
Subject: [PATCH 267/293] Replace deprecated sprintf with snprintf in API
 protobuf code generation (#9365)

---
 esphome/components/api/api_pb2_dump.cpp | 536 ++++++++++++------------
 script/api_protobuf/api_protobuf.py     |  24 +-
 2 files changed, 280 insertions(+), 280 deletions(-)

diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp
index 84e765e40f..48ddd42d61 100644
--- a/esphome/components/api/api_pb2_dump.cpp
+++ b/esphome/components/api/api_pb2_dump.cpp
@@ -600,12 +600,12 @@ void HelloRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  api_version_major: ");
-  sprintf(buffer, "%" PRIu32, this->api_version_major);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_major);
   out.append(buffer);
   out.append("\n");
 
   out.append("  api_version_minor: ");
-  sprintf(buffer, "%" PRIu32, this->api_version_minor);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_minor);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -614,12 +614,12 @@ void HelloResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("HelloResponse {\n");
   out.append("  api_version_major: ");
-  sprintf(buffer, "%" PRIu32, this->api_version_major);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_major);
   out.append(buffer);
   out.append("\n");
 
   out.append("  api_version_minor: ");
-  sprintf(buffer, "%" PRIu32, this->api_version_minor);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_minor);
   out.append(buffer);
   out.append("\n");
 
@@ -657,7 +657,7 @@ void AreaInfo::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("AreaInfo {\n");
   out.append("  area_id: ");
-  sprintf(buffer, "%" PRIu32, this->area_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->area_id);
   out.append(buffer);
   out.append("\n");
 
@@ -670,7 +670,7 @@ void DeviceInfo::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("DeviceInfo {\n");
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
 
@@ -679,7 +679,7 @@ void DeviceInfo::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  area_id: ");
-  sprintf(buffer, "%" PRIu32, this->area_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->area_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -724,17 +724,17 @@ void DeviceInfoResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  webserver_port: ");
-  sprintf(buffer, "%" PRIu32, this->webserver_port);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->webserver_port);
   out.append(buffer);
   out.append("\n");
 
   out.append("  legacy_bluetooth_proxy_version: ");
-  sprintf(buffer, "%" PRIu32, this->legacy_bluetooth_proxy_version);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->legacy_bluetooth_proxy_version);
   out.append(buffer);
   out.append("\n");
 
   out.append("  bluetooth_proxy_feature_flags: ");
-  sprintf(buffer, "%" PRIu32, this->bluetooth_proxy_feature_flags);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->bluetooth_proxy_feature_flags);
   out.append(buffer);
   out.append("\n");
 
@@ -747,12 +747,12 @@ void DeviceInfoResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  legacy_voice_assistant_version: ");
-  sprintf(buffer, "%" PRIu32, this->legacy_voice_assistant_version);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->legacy_voice_assistant_version);
   out.append(buffer);
   out.append("\n");
 
   out.append("  voice_assistant_feature_flags: ");
-  sprintf(buffer, "%" PRIu32, this->voice_assistant_feature_flags);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->voice_assistant_feature_flags);
   out.append(buffer);
   out.append("\n");
 
@@ -797,7 +797,7 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -830,7 +830,7 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -839,7 +839,7 @@ void BinarySensorStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BinarySensorStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -852,7 +852,7 @@ void BinarySensorStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -867,7 +867,7 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -912,7 +912,7 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -921,7 +921,7 @@ void CoverStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("CoverStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -930,12 +930,12 @@ void CoverStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  position: ");
-  sprintf(buffer, "%g", this->position);
+  snprintf(buffer, sizeof(buffer), "%g", this->position);
   out.append(buffer);
   out.append("\n");
 
   out.append("  tilt: ");
-  sprintf(buffer, "%g", this->tilt);
+  snprintf(buffer, sizeof(buffer), "%g", this->tilt);
   out.append(buffer);
   out.append("\n");
 
@@ -944,7 +944,7 @@ void CoverStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -953,7 +953,7 @@ void CoverCommandRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("CoverCommandRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -970,7 +970,7 @@ void CoverCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  position: ");
-  sprintf(buffer, "%g", this->position);
+  snprintf(buffer, sizeof(buffer), "%g", this->position);
   out.append(buffer);
   out.append("\n");
 
@@ -979,7 +979,7 @@ void CoverCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  tilt: ");
-  sprintf(buffer, "%g", this->tilt);
+  snprintf(buffer, sizeof(buffer), "%g", this->tilt);
   out.append(buffer);
   out.append("\n");
 
@@ -998,7 +998,7 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -1023,7 +1023,7 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  supported_speed_count: ");
-  sprintf(buffer, "%" PRId32, this->supported_speed_count);
+  snprintf(buffer, sizeof(buffer), "%" PRId32, this->supported_speed_count);
   out.append(buffer);
   out.append("\n");
 
@@ -1046,7 +1046,7 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const {
   }
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -1055,7 +1055,7 @@ void FanStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("FanStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -1076,7 +1076,7 @@ void FanStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  speed_level: ");
-  sprintf(buffer, "%" PRId32, this->speed_level);
+  snprintf(buffer, sizeof(buffer), "%" PRId32, this->speed_level);
   out.append(buffer);
   out.append("\n");
 
@@ -1085,7 +1085,7 @@ void FanStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -1094,7 +1094,7 @@ void FanCommandRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("FanCommandRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -1135,7 +1135,7 @@ void FanCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  speed_level: ");
-  sprintf(buffer, "%" PRId32, this->speed_level);
+  snprintf(buffer, sizeof(buffer), "%" PRId32, this->speed_level);
   out.append(buffer);
   out.append("\n");
 
@@ -1158,7 +1158,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -1193,12 +1193,12 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  min_mireds: ");
-  sprintf(buffer, "%g", this->min_mireds);
+  snprintf(buffer, sizeof(buffer), "%g", this->min_mireds);
   out.append(buffer);
   out.append("\n");
 
   out.append("  max_mireds: ");
-  sprintf(buffer, "%g", this->max_mireds);
+  snprintf(buffer, sizeof(buffer), "%g", this->max_mireds);
   out.append(buffer);
   out.append("\n");
 
@@ -1221,7 +1221,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -1230,7 +1230,7 @@ void LightStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("LightStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -1239,7 +1239,7 @@ void LightStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  brightness: ");
-  sprintf(buffer, "%g", this->brightness);
+  snprintf(buffer, sizeof(buffer), "%g", this->brightness);
   out.append(buffer);
   out.append("\n");
 
@@ -1248,42 +1248,42 @@ void LightStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  color_brightness: ");
-  sprintf(buffer, "%g", this->color_brightness);
+  snprintf(buffer, sizeof(buffer), "%g", this->color_brightness);
   out.append(buffer);
   out.append("\n");
 
   out.append("  red: ");
-  sprintf(buffer, "%g", this->red);
+  snprintf(buffer, sizeof(buffer), "%g", this->red);
   out.append(buffer);
   out.append("\n");
 
   out.append("  green: ");
-  sprintf(buffer, "%g", this->green);
+  snprintf(buffer, sizeof(buffer), "%g", this->green);
   out.append(buffer);
   out.append("\n");
 
   out.append("  blue: ");
-  sprintf(buffer, "%g", this->blue);
+  snprintf(buffer, sizeof(buffer), "%g", this->blue);
   out.append(buffer);
   out.append("\n");
 
   out.append("  white: ");
-  sprintf(buffer, "%g", this->white);
+  snprintf(buffer, sizeof(buffer), "%g", this->white);
   out.append(buffer);
   out.append("\n");
 
   out.append("  color_temperature: ");
-  sprintf(buffer, "%g", this->color_temperature);
+  snprintf(buffer, sizeof(buffer), "%g", this->color_temperature);
   out.append(buffer);
   out.append("\n");
 
   out.append("  cold_white: ");
-  sprintf(buffer, "%g", this->cold_white);
+  snprintf(buffer, sizeof(buffer), "%g", this->cold_white);
   out.append(buffer);
   out.append("\n");
 
   out.append("  warm_white: ");
-  sprintf(buffer, "%g", this->warm_white);
+  snprintf(buffer, sizeof(buffer), "%g", this->warm_white);
   out.append(buffer);
   out.append("\n");
 
@@ -1292,7 +1292,7 @@ void LightStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -1301,7 +1301,7 @@ void LightCommandRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("LightCommandRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -1318,7 +1318,7 @@ void LightCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  brightness: ");
-  sprintf(buffer, "%g", this->brightness);
+  snprintf(buffer, sizeof(buffer), "%g", this->brightness);
   out.append(buffer);
   out.append("\n");
 
@@ -1335,7 +1335,7 @@ void LightCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  color_brightness: ");
-  sprintf(buffer, "%g", this->color_brightness);
+  snprintf(buffer, sizeof(buffer), "%g", this->color_brightness);
   out.append(buffer);
   out.append("\n");
 
@@ -1344,17 +1344,17 @@ void LightCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  red: ");
-  sprintf(buffer, "%g", this->red);
+  snprintf(buffer, sizeof(buffer), "%g", this->red);
   out.append(buffer);
   out.append("\n");
 
   out.append("  green: ");
-  sprintf(buffer, "%g", this->green);
+  snprintf(buffer, sizeof(buffer), "%g", this->green);
   out.append(buffer);
   out.append("\n");
 
   out.append("  blue: ");
-  sprintf(buffer, "%g", this->blue);
+  snprintf(buffer, sizeof(buffer), "%g", this->blue);
   out.append(buffer);
   out.append("\n");
 
@@ -1363,7 +1363,7 @@ void LightCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  white: ");
-  sprintf(buffer, "%g", this->white);
+  snprintf(buffer, sizeof(buffer), "%g", this->white);
   out.append(buffer);
   out.append("\n");
 
@@ -1372,7 +1372,7 @@ void LightCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  color_temperature: ");
-  sprintf(buffer, "%g", this->color_temperature);
+  snprintf(buffer, sizeof(buffer), "%g", this->color_temperature);
   out.append(buffer);
   out.append("\n");
 
@@ -1381,7 +1381,7 @@ void LightCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  cold_white: ");
-  sprintf(buffer, "%g", this->cold_white);
+  snprintf(buffer, sizeof(buffer), "%g", this->cold_white);
   out.append(buffer);
   out.append("\n");
 
@@ -1390,7 +1390,7 @@ void LightCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  warm_white: ");
-  sprintf(buffer, "%g", this->warm_white);
+  snprintf(buffer, sizeof(buffer), "%g", this->warm_white);
   out.append(buffer);
   out.append("\n");
 
@@ -1399,7 +1399,7 @@ void LightCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  transition_length: ");
-  sprintf(buffer, "%" PRIu32, this->transition_length);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->transition_length);
   out.append(buffer);
   out.append("\n");
 
@@ -1408,7 +1408,7 @@ void LightCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  flash_length: ");
-  sprintf(buffer, "%" PRIu32, this->flash_length);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flash_length);
   out.append(buffer);
   out.append("\n");
 
@@ -1431,7 +1431,7 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -1452,7 +1452,7 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  accuracy_decimals: ");
-  sprintf(buffer, "%" PRId32, this->accuracy_decimals);
+  snprintf(buffer, sizeof(buffer), "%" PRId32, this->accuracy_decimals);
   out.append(buffer);
   out.append("\n");
 
@@ -1481,7 +1481,7 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -1490,12 +1490,12 @@ void SensorStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("SensorStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
   out.append("  state: ");
-  sprintf(buffer, "%g", this->state);
+  snprintf(buffer, sizeof(buffer), "%g", this->state);
   out.append(buffer);
   out.append("\n");
 
@@ -1504,7 +1504,7 @@ void SensorStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -1519,7 +1519,7 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -1552,7 +1552,7 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -1561,7 +1561,7 @@ void SwitchStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("SwitchStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -1570,7 +1570,7 @@ void SwitchStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -1579,7 +1579,7 @@ void SwitchCommandRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("SwitchCommandRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -1598,7 +1598,7 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -1627,7 +1627,7 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -1636,7 +1636,7 @@ void TextSensorStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("TextSensorStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -1649,7 +1649,7 @@ void TextSensorStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -1786,7 +1786,7 @@ void GetTimeResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("GetTimeResponse {\n");
   out.append("  epoch_seconds: ");
-  sprintf(buffer, "%" PRIu32, this->epoch_seconds);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -1811,7 +1811,7 @@ void ListEntitiesServicesResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -1830,12 +1830,12 @@ void ExecuteServiceArgument::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  legacy_int: ");
-  sprintf(buffer, "%" PRId32, this->legacy_int);
+  snprintf(buffer, sizeof(buffer), "%" PRId32, this->legacy_int);
   out.append(buffer);
   out.append("\n");
 
   out.append("  float_: ");
-  sprintf(buffer, "%g", this->float_);
+  snprintf(buffer, sizeof(buffer), "%g", this->float_);
   out.append(buffer);
   out.append("\n");
 
@@ -1844,7 +1844,7 @@ void ExecuteServiceArgument::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  int_: ");
-  sprintf(buffer, "%" PRId32, this->int_);
+  snprintf(buffer, sizeof(buffer), "%" PRId32, this->int_);
   out.append(buffer);
   out.append("\n");
 
@@ -1856,14 +1856,14 @@ void ExecuteServiceArgument::dump_to(std::string &out) const {
 
   for (const auto &it : this->int_array) {
     out.append("  int_array: ");
-    sprintf(buffer, "%" PRId32, it);
+    snprintf(buffer, sizeof(buffer), "%" PRId32, it);
     out.append(buffer);
     out.append("\n");
   }
 
   for (const auto &it : this->float_array) {
     out.append("  float_array: ");
-    sprintf(buffer, "%g", it);
+    snprintf(buffer, sizeof(buffer), "%g", it);
     out.append(buffer);
     out.append("\n");
   }
@@ -1879,7 +1879,7 @@ void ExecuteServiceRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("ExecuteServiceRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -1899,7 +1899,7 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -1924,7 +1924,7 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -1933,7 +1933,7 @@ void CameraImageResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("CameraImageResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -1968,7 +1968,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -1995,17 +1995,17 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
   }
 
   out.append("  visual_min_temperature: ");
-  sprintf(buffer, "%g", this->visual_min_temperature);
+  snprintf(buffer, sizeof(buffer), "%g", this->visual_min_temperature);
   out.append(buffer);
   out.append("\n");
 
   out.append("  visual_max_temperature: ");
-  sprintf(buffer, "%g", this->visual_max_temperature);
+  snprintf(buffer, sizeof(buffer), "%g", this->visual_max_temperature);
   out.append(buffer);
   out.append("\n");
 
   out.append("  visual_target_temperature_step: ");
-  sprintf(buffer, "%g", this->visual_target_temperature_step);
+  snprintf(buffer, sizeof(buffer), "%g", this->visual_target_temperature_step);
   out.append(buffer);
   out.append("\n");
 
@@ -2060,7 +2060,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  visual_current_temperature_step: ");
-  sprintf(buffer, "%g", this->visual_current_temperature_step);
+  snprintf(buffer, sizeof(buffer), "%g", this->visual_current_temperature_step);
   out.append(buffer);
   out.append("\n");
 
@@ -2073,17 +2073,17 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  visual_min_humidity: ");
-  sprintf(buffer, "%g", this->visual_min_humidity);
+  snprintf(buffer, sizeof(buffer), "%g", this->visual_min_humidity);
   out.append(buffer);
   out.append("\n");
 
   out.append("  visual_max_humidity: ");
-  sprintf(buffer, "%g", this->visual_max_humidity);
+  snprintf(buffer, sizeof(buffer), "%g", this->visual_max_humidity);
   out.append(buffer);
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2092,7 +2092,7 @@ void ClimateStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("ClimateStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -2101,22 +2101,22 @@ void ClimateStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  current_temperature: ");
-  sprintf(buffer, "%g", this->current_temperature);
+  snprintf(buffer, sizeof(buffer), "%g", this->current_temperature);
   out.append(buffer);
   out.append("\n");
 
   out.append("  target_temperature: ");
-  sprintf(buffer, "%g", this->target_temperature);
+  snprintf(buffer, sizeof(buffer), "%g", this->target_temperature);
   out.append(buffer);
   out.append("\n");
 
   out.append("  target_temperature_low: ");
-  sprintf(buffer, "%g", this->target_temperature_low);
+  snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_low);
   out.append(buffer);
   out.append("\n");
 
   out.append("  target_temperature_high: ");
-  sprintf(buffer, "%g", this->target_temperature_high);
+  snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_high);
   out.append(buffer);
   out.append("\n");
 
@@ -2149,17 +2149,17 @@ void ClimateStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  current_humidity: ");
-  sprintf(buffer, "%g", this->current_humidity);
+  snprintf(buffer, sizeof(buffer), "%g", this->current_humidity);
   out.append(buffer);
   out.append("\n");
 
   out.append("  target_humidity: ");
-  sprintf(buffer, "%g", this->target_humidity);
+  snprintf(buffer, sizeof(buffer), "%g", this->target_humidity);
   out.append(buffer);
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2168,7 +2168,7 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("ClimateCommandRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -2185,7 +2185,7 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  target_temperature: ");
-  sprintf(buffer, "%g", this->target_temperature);
+  snprintf(buffer, sizeof(buffer), "%g", this->target_temperature);
   out.append(buffer);
   out.append("\n");
 
@@ -2194,7 +2194,7 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  target_temperature_low: ");
-  sprintf(buffer, "%g", this->target_temperature_low);
+  snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_low);
   out.append(buffer);
   out.append("\n");
 
@@ -2203,7 +2203,7 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  target_temperature_high: ");
-  sprintf(buffer, "%g", this->target_temperature_high);
+  snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_high);
   out.append(buffer);
   out.append("\n");
 
@@ -2260,7 +2260,7 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  target_humidity: ");
-  sprintf(buffer, "%g", this->target_humidity);
+  snprintf(buffer, sizeof(buffer), "%g", this->target_humidity);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2275,7 +2275,7 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -2292,17 +2292,17 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  min_value: ");
-  sprintf(buffer, "%g", this->min_value);
+  snprintf(buffer, sizeof(buffer), "%g", this->min_value);
   out.append(buffer);
   out.append("\n");
 
   out.append("  max_value: ");
-  sprintf(buffer, "%g", this->max_value);
+  snprintf(buffer, sizeof(buffer), "%g", this->max_value);
   out.append(buffer);
   out.append("\n");
 
   out.append("  step: ");
-  sprintf(buffer, "%g", this->step);
+  snprintf(buffer, sizeof(buffer), "%g", this->step);
   out.append(buffer);
   out.append("\n");
 
@@ -2327,7 +2327,7 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2336,12 +2336,12 @@ void NumberStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("NumberStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
   out.append("  state: ");
-  sprintf(buffer, "%g", this->state);
+  snprintf(buffer, sizeof(buffer), "%g", this->state);
   out.append(buffer);
   out.append("\n");
 
@@ -2350,7 +2350,7 @@ void NumberStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2359,12 +2359,12 @@ void NumberCommandRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("NumberCommandRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
   out.append("  state: ");
-  sprintf(buffer, "%g", this->state);
+  snprintf(buffer, sizeof(buffer), "%g", this->state);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2379,7 +2379,7 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -2410,7 +2410,7 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2419,7 +2419,7 @@ void SelectStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("SelectStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -2432,7 +2432,7 @@ void SelectStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2441,7 +2441,7 @@ void SelectCommandRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("SelectCommandRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -2460,7 +2460,7 @@ void ListEntitiesSirenResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -2499,7 +2499,7 @@ void ListEntitiesSirenResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2508,7 +2508,7 @@ void SirenStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("SirenStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -2517,7 +2517,7 @@ void SirenStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2526,7 +2526,7 @@ void SirenCommandRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("SirenCommandRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -2551,7 +2551,7 @@ void SirenCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  duration: ");
-  sprintf(buffer, "%" PRIu32, this->duration);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->duration);
   out.append(buffer);
   out.append("\n");
 
@@ -2560,7 +2560,7 @@ void SirenCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  volume: ");
-  sprintf(buffer, "%g", this->volume);
+  snprintf(buffer, sizeof(buffer), "%g", this->volume);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2575,7 +2575,7 @@ void ListEntitiesLockResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -2616,7 +2616,7 @@ void ListEntitiesLockResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2625,7 +2625,7 @@ void LockStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("LockStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -2634,7 +2634,7 @@ void LockStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2643,7 +2643,7 @@ void LockCommandRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("LockCommandRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -2670,7 +2670,7 @@ void ListEntitiesButtonResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -2699,7 +2699,7 @@ void ListEntitiesButtonResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2708,7 +2708,7 @@ void ButtonCommandRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("ButtonCommandRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2723,12 +2723,12 @@ void MediaPlayerSupportedFormat::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  sample_rate: ");
-  sprintf(buffer, "%" PRIu32, this->sample_rate);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->sample_rate);
   out.append(buffer);
   out.append("\n");
 
   out.append("  num_channels: ");
-  sprintf(buffer, "%" PRIu32, this->num_channels);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->num_channels);
   out.append(buffer);
   out.append("\n");
 
@@ -2737,7 +2737,7 @@ void MediaPlayerSupportedFormat::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  sample_bytes: ");
-  sprintf(buffer, "%" PRIu32, this->sample_bytes);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->sample_bytes);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2750,7 +2750,7 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -2785,7 +2785,7 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const {
   }
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2794,7 +2794,7 @@ void MediaPlayerStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("MediaPlayerStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -2803,7 +2803,7 @@ void MediaPlayerStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  volume: ");
-  sprintf(buffer, "%g", this->volume);
+  snprintf(buffer, sizeof(buffer), "%g", this->volume);
   out.append(buffer);
   out.append("\n");
 
@@ -2812,7 +2812,7 @@ void MediaPlayerStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2821,7 +2821,7 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("MediaPlayerCommandRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -2838,7 +2838,7 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  volume: ");
-  sprintf(buffer, "%g", this->volume);
+  snprintf(buffer, sizeof(buffer), "%g", this->volume);
   out.append(buffer);
   out.append("\n");
 
@@ -2865,7 +2865,7 @@ void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const
   __attribute__((unused)) char buffer[64];
   out.append("SubscribeBluetoothLEAdvertisementsRequest {\n");
   out.append("  flags: ");
-  sprintf(buffer, "%" PRIu32, this->flags);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flags);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2879,7 +2879,7 @@ void BluetoothServiceData::dump_to(std::string &out) const {
 
   for (const auto &it : this->legacy_data) {
     out.append("  legacy_data: ");
-    sprintf(buffer, "%" PRIu32, it);
+    snprintf(buffer, sizeof(buffer), "%" PRIu32, it);
     out.append(buffer);
     out.append("\n");
   }
@@ -2893,7 +2893,7 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothLEAdvertisementResponse {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
 
@@ -2902,7 +2902,7 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  rssi: ");
-  sprintf(buffer, "%" PRId32, this->rssi);
+  snprintf(buffer, sizeof(buffer), "%" PRId32, this->rssi);
   out.append(buffer);
   out.append("\n");
 
@@ -2925,7 +2925,7 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const {
   }
 
   out.append("  address_type: ");
-  sprintf(buffer, "%" PRIu32, this->address_type);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2934,17 +2934,17 @@ void BluetoothLERawAdvertisement::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothLERawAdvertisement {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
 
   out.append("  rssi: ");
-  sprintf(buffer, "%" PRId32, this->rssi);
+  snprintf(buffer, sizeof(buffer), "%" PRId32, this->rssi);
   out.append(buffer);
   out.append("\n");
 
   out.append("  address_type: ");
-  sprintf(buffer, "%" PRIu32, this->address_type);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type);
   out.append(buffer);
   out.append("\n");
 
@@ -2967,7 +2967,7 @@ void BluetoothDeviceRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothDeviceRequest {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
 
@@ -2980,7 +2980,7 @@ void BluetoothDeviceRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  address_type: ");
-  sprintf(buffer, "%" PRIu32, this->address_type);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -2989,7 +2989,7 @@ void BluetoothDeviceConnectionResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothDeviceConnectionResponse {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
 
@@ -2998,12 +2998,12 @@ void BluetoothDeviceConnectionResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  mtu: ");
-  sprintf(buffer, "%" PRIu32, this->mtu);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->mtu);
   out.append(buffer);
   out.append("\n");
 
   out.append("  error: ");
-  sprintf(buffer, "%" PRId32, this->error);
+  snprintf(buffer, sizeof(buffer), "%" PRId32, this->error);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3012,7 +3012,7 @@ void BluetoothGATTGetServicesRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothGATTGetServicesRequest {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3022,13 +3022,13 @@ void BluetoothGATTDescriptor::dump_to(std::string &out) const {
   out.append("BluetoothGATTDescriptor {\n");
   for (const auto &it : this->uuid) {
     out.append("  uuid: ");
-    sprintf(buffer, "%llu", it);
+    snprintf(buffer, sizeof(buffer), "%llu", it);
     out.append(buffer);
     out.append("\n");
   }
 
   out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3038,18 +3038,18 @@ void BluetoothGATTCharacteristic::dump_to(std::string &out) const {
   out.append("BluetoothGATTCharacteristic {\n");
   for (const auto &it : this->uuid) {
     out.append("  uuid: ");
-    sprintf(buffer, "%llu", it);
+    snprintf(buffer, sizeof(buffer), "%llu", it);
     out.append(buffer);
     out.append("\n");
   }
 
   out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle);
   out.append(buffer);
   out.append("\n");
 
   out.append("  properties: ");
-  sprintf(buffer, "%" PRIu32, this->properties);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->properties);
   out.append(buffer);
   out.append("\n");
 
@@ -3065,13 +3065,13 @@ void BluetoothGATTService::dump_to(std::string &out) const {
   out.append("BluetoothGATTService {\n");
   for (const auto &it : this->uuid) {
     out.append("  uuid: ");
-    sprintf(buffer, "%llu", it);
+    snprintf(buffer, sizeof(buffer), "%llu", it);
     out.append(buffer);
     out.append("\n");
   }
 
   out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle);
   out.append(buffer);
   out.append("\n");
 
@@ -3086,7 +3086,7 @@ void BluetoothGATTGetServicesResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothGATTGetServicesResponse {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
 
@@ -3101,7 +3101,7 @@ void BluetoothGATTGetServicesDoneResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothGATTGetServicesDoneResponse {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3110,12 +3110,12 @@ void BluetoothGATTReadRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothGATTReadRequest {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
 
   out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3124,12 +3124,12 @@ void BluetoothGATTReadResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothGATTReadResponse {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
 
   out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle);
   out.append(buffer);
   out.append("\n");
 
@@ -3142,12 +3142,12 @@ void BluetoothGATTWriteRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothGATTWriteRequest {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
 
   out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle);
   out.append(buffer);
   out.append("\n");
 
@@ -3164,12 +3164,12 @@ void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothGATTReadDescriptorRequest {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
 
   out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3178,12 +3178,12 @@ void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothGATTWriteDescriptorRequest {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
 
   out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle);
   out.append(buffer);
   out.append("\n");
 
@@ -3196,12 +3196,12 @@ void BluetoothGATTNotifyRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothGATTNotifyRequest {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
 
   out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle);
   out.append(buffer);
   out.append("\n");
 
@@ -3214,12 +3214,12 @@ void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothGATTNotifyDataResponse {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
 
   out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle);
   out.append(buffer);
   out.append("\n");
 
@@ -3235,18 +3235,18 @@ void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothConnectionsFreeResponse {\n");
   out.append("  free: ");
-  sprintf(buffer, "%" PRIu32, this->free);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->free);
   out.append(buffer);
   out.append("\n");
 
   out.append("  limit: ");
-  sprintf(buffer, "%" PRIu32, this->limit);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->limit);
   out.append(buffer);
   out.append("\n");
 
   for (const auto &it : this->allocated) {
     out.append("  allocated: ");
-    sprintf(buffer, "%llu", it);
+    snprintf(buffer, sizeof(buffer), "%llu", it);
     out.append(buffer);
     out.append("\n");
   }
@@ -3256,17 +3256,17 @@ void BluetoothGATTErrorResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothGATTErrorResponse {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
 
   out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle);
   out.append(buffer);
   out.append("\n");
 
   out.append("  error: ");
-  sprintf(buffer, "%" PRId32, this->error);
+  snprintf(buffer, sizeof(buffer), "%" PRId32, this->error);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3275,12 +3275,12 @@ void BluetoothGATTWriteResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothGATTWriteResponse {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
 
   out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3289,12 +3289,12 @@ void BluetoothGATTNotifyResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothGATTNotifyResponse {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
 
   out.append("  handle: ");
-  sprintf(buffer, "%" PRIu32, this->handle);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3303,7 +3303,7 @@ void BluetoothDevicePairingResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothDevicePairingResponse {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
 
@@ -3312,7 +3312,7 @@ void BluetoothDevicePairingResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  error: ");
-  sprintf(buffer, "%" PRId32, this->error);
+  snprintf(buffer, sizeof(buffer), "%" PRId32, this->error);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3321,7 +3321,7 @@ void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothDeviceUnpairingResponse {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
 
@@ -3330,7 +3330,7 @@ void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  error: ");
-  sprintf(buffer, "%" PRId32, this->error);
+  snprintf(buffer, sizeof(buffer), "%" PRId32, this->error);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3342,7 +3342,7 @@ void BluetoothDeviceClearCacheResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("BluetoothDeviceClearCacheResponse {\n");
   out.append("  address: ");
-  sprintf(buffer, "%llu", this->address);
+  snprintf(buffer, sizeof(buffer), "%llu", this->address);
   out.append(buffer);
   out.append("\n");
 
@@ -3351,7 +3351,7 @@ void BluetoothDeviceClearCacheResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  error: ");
-  sprintf(buffer, "%" PRId32, this->error);
+  snprintf(buffer, sizeof(buffer), "%" PRId32, this->error);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3386,7 +3386,7 @@ void SubscribeVoiceAssistantRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  flags: ");
-  sprintf(buffer, "%" PRIu32, this->flags);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flags);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3395,17 +3395,17 @@ void VoiceAssistantAudioSettings::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("VoiceAssistantAudioSettings {\n");
   out.append("  noise_suppression_level: ");
-  sprintf(buffer, "%" PRIu32, this->noise_suppression_level);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->noise_suppression_level);
   out.append(buffer);
   out.append("\n");
 
   out.append("  auto_gain: ");
-  sprintf(buffer, "%" PRIu32, this->auto_gain);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->auto_gain);
   out.append(buffer);
   out.append("\n");
 
   out.append("  volume_multiplier: ");
-  sprintf(buffer, "%g", this->volume_multiplier);
+  snprintf(buffer, sizeof(buffer), "%g", this->volume_multiplier);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3422,7 +3422,7 @@ void VoiceAssistantRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  flags: ");
-  sprintf(buffer, "%" PRIu32, this->flags);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flags);
   out.append(buffer);
   out.append("\n");
 
@@ -3439,7 +3439,7 @@ void VoiceAssistantResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("VoiceAssistantResponse {\n");
   out.append("  port: ");
-  sprintf(buffer, "%" PRIu32, this->port);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->port);
   out.append(buffer);
   out.append("\n");
 
@@ -3502,12 +3502,12 @@ void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  total_seconds: ");
-  sprintf(buffer, "%" PRIu32, this->total_seconds);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->total_seconds);
   out.append(buffer);
   out.append("\n");
 
   out.append("  seconds_left: ");
-  sprintf(buffer, "%" PRIu32, this->seconds_left);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->seconds_left);
   out.append(buffer);
   out.append("\n");
 
@@ -3581,7 +3581,7 @@ void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const {
   }
 
   out.append("  max_active_wake_words: ");
-  sprintf(buffer, "%" PRIu32, this->max_active_wake_words);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->max_active_wake_words);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3606,7 +3606,7 @@ void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -3631,7 +3631,7 @@ void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  supported_features: ");
-  sprintf(buffer, "%" PRIu32, this->supported_features);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->supported_features);
   out.append(buffer);
   out.append("\n");
 
@@ -3644,7 +3644,7 @@ void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3653,7 +3653,7 @@ void AlarmControlPanelStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("AlarmControlPanelStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -3662,7 +3662,7 @@ void AlarmControlPanelStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3671,7 +3671,7 @@ void AlarmControlPanelCommandRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("AlarmControlPanelCommandRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -3694,7 +3694,7 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -3719,12 +3719,12 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  min_length: ");
-  sprintf(buffer, "%" PRIu32, this->min_length);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->min_length);
   out.append(buffer);
   out.append("\n");
 
   out.append("  max_length: ");
-  sprintf(buffer, "%" PRIu32, this->max_length);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->max_length);
   out.append(buffer);
   out.append("\n");
 
@@ -3737,7 +3737,7 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3746,7 +3746,7 @@ void TextStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("TextStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -3759,7 +3759,7 @@ void TextStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3768,7 +3768,7 @@ void TextCommandRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("TextCommandRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -3787,7 +3787,7 @@ void ListEntitiesDateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -3812,7 +3812,7 @@ void ListEntitiesDateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3821,7 +3821,7 @@ void DateStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("DateStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -3830,22 +3830,22 @@ void DateStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  year: ");
-  sprintf(buffer, "%" PRIu32, this->year);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->year);
   out.append(buffer);
   out.append("\n");
 
   out.append("  month: ");
-  sprintf(buffer, "%" PRIu32, this->month);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->month);
   out.append(buffer);
   out.append("\n");
 
   out.append("  day: ");
-  sprintf(buffer, "%" PRIu32, this->day);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->day);
   out.append(buffer);
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3854,22 +3854,22 @@ void DateCommandRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("DateCommandRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
   out.append("  year: ");
-  sprintf(buffer, "%" PRIu32, this->year);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->year);
   out.append(buffer);
   out.append("\n");
 
   out.append("  month: ");
-  sprintf(buffer, "%" PRIu32, this->month);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->month);
   out.append(buffer);
   out.append("\n");
 
   out.append("  day: ");
-  sprintf(buffer, "%" PRIu32, this->day);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->day);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3884,7 +3884,7 @@ void ListEntitiesTimeResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -3909,7 +3909,7 @@ void ListEntitiesTimeResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3918,7 +3918,7 @@ void TimeStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("TimeStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -3927,22 +3927,22 @@ void TimeStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  hour: ");
-  sprintf(buffer, "%" PRIu32, this->hour);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->hour);
   out.append(buffer);
   out.append("\n");
 
   out.append("  minute: ");
-  sprintf(buffer, "%" PRIu32, this->minute);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->minute);
   out.append(buffer);
   out.append("\n");
 
   out.append("  second: ");
-  sprintf(buffer, "%" PRIu32, this->second);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->second);
   out.append(buffer);
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3951,22 +3951,22 @@ void TimeCommandRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("TimeCommandRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
   out.append("  hour: ");
-  sprintf(buffer, "%" PRIu32, this->hour);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->hour);
   out.append(buffer);
   out.append("\n");
 
   out.append("  minute: ");
-  sprintf(buffer, "%" PRIu32, this->minute);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->minute);
   out.append(buffer);
   out.append("\n");
 
   out.append("  second: ");
-  sprintf(buffer, "%" PRIu32, this->second);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->second);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -3981,7 +3981,7 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -4016,7 +4016,7 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const {
   }
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -4025,7 +4025,7 @@ void EventResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("EventResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -4034,7 +4034,7 @@ void EventResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -4049,7 +4049,7 @@ void ListEntitiesValveResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -4090,7 +4090,7 @@ void ListEntitiesValveResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -4099,12 +4099,12 @@ void ValveStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("ValveStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
   out.append("  position: ");
-  sprintf(buffer, "%g", this->position);
+  snprintf(buffer, sizeof(buffer), "%g", this->position);
   out.append(buffer);
   out.append("\n");
 
@@ -4113,7 +4113,7 @@ void ValveStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -4122,7 +4122,7 @@ void ValveCommandRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("ValveCommandRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -4131,7 +4131,7 @@ void ValveCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  position: ");
-  sprintf(buffer, "%g", this->position);
+  snprintf(buffer, sizeof(buffer), "%g", this->position);
   out.append(buffer);
   out.append("\n");
 
@@ -4150,7 +4150,7 @@ void ListEntitiesDateTimeResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -4175,7 +4175,7 @@ void ListEntitiesDateTimeResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -4184,7 +4184,7 @@ void DateTimeStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("DateTimeStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -4193,12 +4193,12 @@ void DateTimeStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  epoch_seconds: ");
-  sprintf(buffer, "%" PRIu32, this->epoch_seconds);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds);
   out.append(buffer);
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -4207,12 +4207,12 @@ void DateTimeCommandRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("DateTimeCommandRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
   out.append("  epoch_seconds: ");
-  sprintf(buffer, "%" PRIu32, this->epoch_seconds);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -4227,7 +4227,7 @@ void ListEntitiesUpdateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -4256,7 +4256,7 @@ void ListEntitiesUpdateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -4265,7 +4265,7 @@ void UpdateStateResponse::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("UpdateStateResponse {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
@@ -4282,7 +4282,7 @@ void UpdateStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  progress: ");
-  sprintf(buffer, "%g", this->progress);
+  snprintf(buffer, sizeof(buffer), "%g", this->progress);
   out.append(buffer);
   out.append("\n");
 
@@ -4307,7 +4307,7 @@ void UpdateStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  device_id: ");
-  sprintf(buffer, "%" PRIu32, this->device_id);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
   out.append(buffer);
   out.append("\n");
   out.append("}");
@@ -4316,7 +4316,7 @@ void UpdateCommandRequest::dump_to(std::string &out) const {
   __attribute__((unused)) char buffer[64];
   out.append("UpdateCommandRequest {\n");
   out.append("  key: ");
-  sprintf(buffer, "%" PRIu32, this->key);
+  snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
   out.append(buffer);
   out.append("\n");
 
diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py
index 2266dda81c..df1f3f8caa 100755
--- a/script/api_protobuf/api_protobuf.py
+++ b/script/api_protobuf/api_protobuf.py
@@ -290,7 +290,7 @@ class DoubleType(TypeInfo):
     wire_type = WireType.FIXED64  # Uses wire type 1 according to protobuf spec
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%g", {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%g", {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -312,7 +312,7 @@ class FloatType(TypeInfo):
     wire_type = WireType.FIXED32  # Uses wire type 5
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%g", {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%g", {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -334,7 +334,7 @@ class Int64Type(TypeInfo):
     wire_type = WireType.VARINT  # Uses wire type 0
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%lld", {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -356,7 +356,7 @@ class UInt64Type(TypeInfo):
     wire_type = WireType.VARINT  # Uses wire type 0
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%llu", {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -378,7 +378,7 @@ class Int32Type(TypeInfo):
     wire_type = WireType.VARINT  # Uses wire type 0
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%" PRId32, {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%" PRId32, {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -400,7 +400,7 @@ class Fixed64Type(TypeInfo):
     wire_type = WireType.FIXED64  # Uses wire type 1
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%llu", {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -422,7 +422,7 @@ class Fixed32Type(TypeInfo):
     wire_type = WireType.FIXED32  # Uses wire type 5
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%" PRIu32, {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%" PRIu32, {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -555,7 +555,7 @@ class UInt32Type(TypeInfo):
     wire_type = WireType.VARINT  # Uses wire type 0
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%" PRIu32, {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%" PRIu32, {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -607,7 +607,7 @@ class SFixed32Type(TypeInfo):
     wire_type = WireType.FIXED32  # Uses wire type 5
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%" PRId32, {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%" PRId32, {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -629,7 +629,7 @@ class SFixed64Type(TypeInfo):
     wire_type = WireType.FIXED64  # Uses wire type 1
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%lld", {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -651,7 +651,7 @@ class SInt32Type(TypeInfo):
     wire_type = WireType.VARINT  # Uses wire type 0
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%" PRId32, {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%" PRId32, {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -673,7 +673,7 @@ class SInt64Type(TypeInfo):
     wire_type = WireType.VARINT  # Uses wire type 0
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%lld", {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n'
         o += "out.append(buffer);"
         return o
 

From 3976fd02eaea2faaa56954b15772c5ed33a007b8 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 15:39:13 -0500
Subject: [PATCH 268/293] Refactor duplicate socket read error handling in API
 frame helper (#9370)

---
 esphome/components/api/api_frame_helper.cpp | 72 ++++++++-------------
 esphome/components/api/api_frame_helper.h   |  3 +
 2 files changed, 31 insertions(+), 44 deletions(-)

diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp
index 6ed9c95354..2f5acc3bfa 100644
--- a/esphome/components/api/api_frame_helper.cpp
+++ b/esphome/components/api/api_frame_helper.cpp
@@ -225,6 +225,22 @@ APIError APIFrameHelper::init_common_() {
 }
 
 #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->info_.c_str(), ##__VA_ARGS__)
+
+APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) {
+  if (received == -1) {
+    if (errno == EWOULDBLOCK || errno == EAGAIN) {
+      return APIError::WOULD_BLOCK;
+    }
+    state_ = State::FAILED;
+    HELPER_LOG("Socket read failed with errno %d", errno);
+    return APIError::SOCKET_READ_FAILED;
+  } else if (received == 0) {
+    state_ = State::FAILED;
+    HELPER_LOG("Connection closed");
+    return APIError::CONNECTION_CLOSED;
+  }
+  return APIError::OK;
+}
 // uncomment to log raw packets
 //#define HELPER_LOG_PACKETS
 
@@ -327,17 +343,9 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
     // no header information yet
     uint8_t to_read = 3 - rx_header_buf_len_;
     ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
-    if (received == -1) {
-      if (errno == EWOULDBLOCK || errno == EAGAIN) {
-        return APIError::WOULD_BLOCK;
-      }
-      state_ = State::FAILED;
-      HELPER_LOG("Socket read failed with errno %d", errno);
-      return APIError::SOCKET_READ_FAILED;
-    } else if (received == 0) {
-      state_ = State::FAILED;
-      HELPER_LOG("Connection closed");
-      return APIError::CONNECTION_CLOSED;
+    APIError err = handle_socket_read_result_(received);
+    if (err != APIError::OK) {
+      return err;
     }
     rx_header_buf_len_ += static_cast(received);
     if (static_cast(received) != to_read) {
@@ -372,17 +380,9 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
     // more data to read
     uint16_t to_read = msg_size - rx_buf_len_;
     ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
-    if (received == -1) {
-      if (errno == EWOULDBLOCK || errno == EAGAIN) {
-        return APIError::WOULD_BLOCK;
-      }
-      state_ = State::FAILED;
-      HELPER_LOG("Socket read failed with errno %d", errno);
-      return APIError::SOCKET_READ_FAILED;
-    } else if (received == 0) {
-      state_ = State::FAILED;
-      HELPER_LOG("Connection closed");
-      return APIError::CONNECTION_CLOSED;
+    APIError err = handle_socket_read_result_(received);
+    if (err != APIError::OK) {
+      return err;
     }
     rx_buf_len_ += static_cast(received);
     if (static_cast(received) != to_read) {
@@ -855,17 +855,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
     // Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time
     ssize_t received =
         this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1);
-    if (received == -1) {
-      if (errno == EWOULDBLOCK || errno == EAGAIN) {
-        return APIError::WOULD_BLOCK;
-      }
-      state_ = State::FAILED;
-      HELPER_LOG("Socket read failed with errno %d", errno);
-      return APIError::SOCKET_READ_FAILED;
-    } else if (received == 0) {
-      state_ = State::FAILED;
-      HELPER_LOG("Connection closed");
-      return APIError::CONNECTION_CLOSED;
+    APIError err = handle_socket_read_result_(received);
+    if (err != APIError::OK) {
+      return err;
     }
 
     // If this was the first read, validate the indicator byte
@@ -949,17 +941,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
     // more data to read
     uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_;
     ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
-    if (received == -1) {
-      if (errno == EWOULDBLOCK || errno == EAGAIN) {
-        return APIError::WOULD_BLOCK;
-      }
-      state_ = State::FAILED;
-      HELPER_LOG("Socket read failed with errno %d", errno);
-      return APIError::SOCKET_READ_FAILED;
-    } else if (received == 0) {
-      state_ = State::FAILED;
-      HELPER_LOG("Connection closed");
-      return APIError::CONNECTION_CLOSED;
+    APIError err = handle_socket_read_result_(received);
+    if (err != APIError::OK) {
+      return err;
     }
     rx_buf_len_ += static_cast(received);
     if (static_cast(received) != to_read) {
diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h
index 1bb6bc7ed3..eae83a3484 100644
--- a/esphome/components/api/api_frame_helper.h
+++ b/esphome/components/api/api_frame_helper.h
@@ -176,6 +176,9 @@ class APIFrameHelper {
 
   // Common initialization for both plaintext and noise protocols
   APIError init_common_();
+
+  // Helper method to handle socket read results
+  APIError handle_socket_read_result_(ssize_t received);
 };
 
 #ifdef USE_API_NOISE

From fe258e10073a88bf41fa70cf29d1adbba4ad62cb Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 15:49:23 -0500
Subject: [PATCH 269/293] Refactor entity lookup methods with macros in
 preparation for device_id support (#9371)

---
 esphome/components/api/api_connection.cpp | 108 ++++---------
 esphome/core/application.h                | 179 ++++------------------
 2 files changed, 62 insertions(+), 225 deletions(-)

diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp
index 51a5769f99..13c5b345b6 100644
--- a/esphome/components/api/api_connection.cpp
+++ b/esphome/components/api/api_connection.cpp
@@ -42,6 +42,19 @@ static const char *const TAG = "api.connection";
 static const int CAMERA_STOP_STREAM = 5000;
 #endif
 
+// Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call object
+#define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \
+  entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
+  if ((entity_var) == nullptr) \
+    return; \
+  auto call = (entity_var)->make_call();
+
+// Helper macro for entity command handlers that don't use make_call() - gets entity by key and returns if not found
+#define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \
+  entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
+  if ((entity_var) == nullptr) \
+    return;
+
 APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent)
     : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
 #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
@@ -361,11 +374,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
   return encode_message_to_buffer(msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 }
 void APIConnection::cover_command(const CoverCommandRequest &msg) {
-  cover::Cover *cover = App.get_cover_by_key(msg.key);
-  if (cover == nullptr)
-    return;
-
-  auto call = cover->make_call();
+  ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover)
   if (msg.has_legacy_command) {
     switch (msg.legacy_command) {
       case enums::LEGACY_COVER_COMMAND_OPEN:
@@ -427,11 +436,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
   return encode_message_to_buffer(msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 }
 void APIConnection::fan_command(const FanCommandRequest &msg) {
-  fan::Fan *fan = App.get_fan_by_key(msg.key);
-  if (fan == nullptr)
-    return;
-
-  auto call = fan->make_call();
+  ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan)
   if (msg.has_state)
     call.set_state(msg.state);
   if (msg.has_oscillating)
@@ -504,11 +509,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
   return encode_message_to_buffer(msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 }
 void APIConnection::light_command(const LightCommandRequest &msg) {
-  light::LightState *light = App.get_light_by_key(msg.key);
-  if (light == nullptr)
-    return;
-
-  auto call = light->make_call();
+  ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light)
   if (msg.has_state)
     call.set_state(msg.state);
   if (msg.has_brightness)
@@ -597,9 +598,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *
   return encode_message_to_buffer(msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 }
 void APIConnection::switch_command(const SwitchCommandRequest &msg) {
-  switch_::Switch *a_switch = App.get_switch_by_key(msg.key);
-  if (a_switch == nullptr)
-    return;
+  ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
 
   if (msg.state) {
     a_switch->turn_on();
@@ -708,11 +707,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
   return encode_message_to_buffer(msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 }
 void APIConnection::climate_command(const ClimateCommandRequest &msg) {
-  climate::Climate *climate = App.get_climate_by_key(msg.key);
-  if (climate == nullptr)
-    return;
-
-  auto call = climate->make_call();
+  ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate)
   if (msg.has_mode)
     call.set_mode(static_cast(msg.mode));
   if (msg.has_target_temperature)
@@ -767,11 +762,7 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *
   return encode_message_to_buffer(msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 }
 void APIConnection::number_command(const NumberCommandRequest &msg) {
-  number::Number *number = App.get_number_by_key(msg.key);
-  if (number == nullptr)
-    return;
-
-  auto call = number->make_call();
+  ENTITY_COMMAND_MAKE_CALL(number::Number, number, number)
   call.set_value(msg.state);
   call.perform();
 }
@@ -801,11 +792,7 @@ uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *co
   return encode_message_to_buffer(msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 }
 void APIConnection::date_command(const DateCommandRequest &msg) {
-  datetime::DateEntity *date = App.get_date_by_key(msg.key);
-  if (date == nullptr)
-    return;
-
-  auto call = date->make_call();
+  ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date)
   call.set_date(msg.year, msg.month, msg.day);
   call.perform();
 }
@@ -835,11 +822,7 @@ uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *co
   return encode_message_to_buffer(msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 }
 void APIConnection::time_command(const TimeCommandRequest &msg) {
-  datetime::TimeEntity *time = App.get_time_by_key(msg.key);
-  if (time == nullptr)
-    return;
-
-  auto call = time->make_call();
+  ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time)
   call.set_time(msg.hour, msg.minute, msg.second);
   call.perform();
 }
@@ -871,11 +854,7 @@ uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection
   return encode_message_to_buffer(msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 }
 void APIConnection::datetime_command(const DateTimeCommandRequest &msg) {
-  datetime::DateTimeEntity *datetime = App.get_datetime_by_key(msg.key);
-  if (datetime == nullptr)
-    return;
-
-  auto call = datetime->make_call();
+  ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime)
   call.set_datetime(msg.epoch_seconds);
   call.perform();
 }
@@ -909,11 +888,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co
   return encode_message_to_buffer(msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 }
 void APIConnection::text_command(const TextCommandRequest &msg) {
-  text::Text *text = App.get_text_by_key(msg.key);
-  if (text == nullptr)
-    return;
-
-  auto call = text->make_call();
+  ENTITY_COMMAND_MAKE_CALL(text::Text, text, text)
   call.set_value(msg.state);
   call.perform();
 }
@@ -945,11 +920,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *
   return encode_message_to_buffer(msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 }
 void APIConnection::select_command(const SelectCommandRequest &msg) {
-  select::Select *select = App.get_select_by_key(msg.key);
-  if (select == nullptr)
-    return;
-
-  auto call = select->make_call();
+  ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
   call.set_option(msg.state);
   call.perform();
 }
@@ -966,10 +937,7 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *
   return encode_message_to_buffer(msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 }
 void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) {
-  button::Button *button = App.get_button_by_key(msg.key);
-  if (button == nullptr)
-    return;
-
+  ENTITY_COMMAND_GET(button::Button, button, button)
   button->press();
 }
 #endif
@@ -1000,9 +968,7 @@ uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *co
   return encode_message_to_buffer(msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 }
 void APIConnection::lock_command(const LockCommandRequest &msg) {
-  lock::Lock *a_lock = App.get_lock_by_key(msg.key);
-  if (a_lock == nullptr)
-    return;
+  ENTITY_COMMAND_GET(lock::Lock, a_lock, lock)
 
   switch (msg.command) {
     case enums::LOCK_UNLOCK:
@@ -1045,11 +1011,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c
   return encode_message_to_buffer(msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 }
 void APIConnection::valve_command(const ValveCommandRequest &msg) {
-  valve::Valve *valve = App.get_valve_by_key(msg.key);
-  if (valve == nullptr)
-    return;
-
-  auto call = valve->make_call();
+  ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve)
   if (msg.has_position)
     call.set_position(msg.position);
   if (msg.stop)
@@ -1096,11 +1058,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
   return encode_message_to_buffer(msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 }
 void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
-  media_player::MediaPlayer *media_player = App.get_media_player_by_key(msg.key);
-  if (media_player == nullptr)
-    return;
-
-  auto call = media_player->make_call();
+  ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player)
   if (msg.has_command) {
     call.set_command(static_cast(msg.command));
   }
@@ -1346,11 +1304,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, AP
                                   is_single);
 }
 void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) {
-  alarm_control_panel::AlarmControlPanel *a_alarm_control_panel = App.get_alarm_control_panel_by_key(msg.key);
-  if (a_alarm_control_panel == nullptr)
-    return;
-
-  auto call = a_alarm_control_panel->make_call();
+  ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel)
   switch (msg.command) {
     case enums::ALARM_CONTROL_PANEL_DISARM:
       call.disarm();
@@ -1438,9 +1392,7 @@ uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *
   return encode_message_to_buffer(msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 }
 void APIConnection::update_command(const UpdateCommandRequest &msg) {
-  update::UpdateEntity *update = App.get_update_by_key(msg.key);
-  if (update == nullptr)
-    return;
+  ENTITY_COMMAND_GET(update::UpdateEntity, update, update)
 
   switch (msg.command) {
     case enums::UPDATE_COMMAND_UPDATE:
diff --git a/esphome/core/application.h b/esphome/core/application.h
index 6ee05309ca..f2b5cb5c89 100644
--- a/esphome/core/application.h
+++ b/esphome/core/application.h
@@ -368,6 +368,17 @@ class Application {
 
   uint8_t get_app_state() const { return this->app_state_; }
 
+// Helper macro for entity getter method declarations - reduces code duplication
+// When USE_DEVICE_ID is enabled in the future, this can be conditionally compiled to add device_id parameter
+#define GET_ENTITY_METHOD(entity_type, entity_name, entities_member) \
+  entity_type *get_##entity_name##_by_key(uint32_t key, bool include_internal = false) { \
+    for (auto *obj : this->entities_member##_) { \
+      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) \
+        return obj; \
+    } \
+    return nullptr; \
+  }
+
 #ifdef USE_DEVICES
   const std::vector &get_devices() { return this->devices_; }
 #endif
@@ -376,218 +387,92 @@ class Application {
 #endif
 #ifdef USE_BINARY_SENSOR
   const std::vector &get_binary_sensors() { return this->binary_sensors_; }
-  binary_sensor::BinarySensor *get_binary_sensor_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->binary_sensors_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(binary_sensor::BinarySensor, binary_sensor, binary_sensors)
 #endif
 #ifdef USE_SWITCH
   const std::vector &get_switches() { return this->switches_; }
-  switch_::Switch *get_switch_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->switches_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(switch_::Switch, switch, switches)
 #endif
 #ifdef USE_BUTTON
   const std::vector &get_buttons() { return this->buttons_; }
-  button::Button *get_button_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->buttons_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(button::Button, button, buttons)
 #endif
 #ifdef USE_SENSOR
   const std::vector &get_sensors() { return this->sensors_; }
-  sensor::Sensor *get_sensor_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->sensors_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(sensor::Sensor, sensor, sensors)
 #endif
 #ifdef USE_TEXT_SENSOR
   const std::vector &get_text_sensors() { return this->text_sensors_; }
-  text_sensor::TextSensor *get_text_sensor_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->text_sensors_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(text_sensor::TextSensor, text_sensor, text_sensors)
 #endif
 #ifdef USE_FAN
   const std::vector &get_fans() { return this->fans_; }
-  fan::Fan *get_fan_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->fans_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(fan::Fan, fan, fans)
 #endif
 #ifdef USE_COVER
   const std::vector &get_covers() { return this->covers_; }
-  cover::Cover *get_cover_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->covers_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(cover::Cover, cover, covers)
 #endif
 #ifdef USE_LIGHT
   const std::vector &get_lights() { return this->lights_; }
-  light::LightState *get_light_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->lights_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(light::LightState, light, lights)
 #endif
 #ifdef USE_CLIMATE
   const std::vector &get_climates() { return this->climates_; }
-  climate::Climate *get_climate_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->climates_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(climate::Climate, climate, climates)
 #endif
 #ifdef USE_NUMBER
   const std::vector &get_numbers() { return this->numbers_; }
-  number::Number *get_number_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->numbers_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(number::Number, number, numbers)
 #endif
 #ifdef USE_DATETIME_DATE
   const std::vector &get_dates() { return this->dates_; }
-  datetime::DateEntity *get_date_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->dates_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(datetime::DateEntity, date, dates)
 #endif
 #ifdef USE_DATETIME_TIME
   const std::vector &get_times() { return this->times_; }
-  datetime::TimeEntity *get_time_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->times_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(datetime::TimeEntity, time, times)
 #endif
 #ifdef USE_DATETIME_DATETIME
   const std::vector &get_datetimes() { return this->datetimes_; }
-  datetime::DateTimeEntity *get_datetime_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->datetimes_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(datetime::DateTimeEntity, datetime, datetimes)
 #endif
 #ifdef USE_TEXT
   const std::vector &get_texts() { return this->texts_; }
-  text::Text *get_text_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->texts_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(text::Text, text, texts)
 #endif
 #ifdef USE_SELECT
   const std::vector &get_selects() { return this->selects_; }
-  select::Select *get_select_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->selects_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(select::Select, select, selects)
 #endif
 #ifdef USE_LOCK
   const std::vector &get_locks() { return this->locks_; }
-  lock::Lock *get_lock_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->locks_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(lock::Lock, lock, locks)
 #endif
 #ifdef USE_VALVE
   const std::vector &get_valves() { return this->valves_; }
-  valve::Valve *get_valve_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->valves_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(valve::Valve, valve, valves)
 #endif
 #ifdef USE_MEDIA_PLAYER
   const std::vector &get_media_players() { return this->media_players_; }
-  media_player::MediaPlayer *get_media_player_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->media_players_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(media_player::MediaPlayer, media_player, media_players)
 #endif
 
 #ifdef USE_ALARM_CONTROL_PANEL
   const std::vector &get_alarm_control_panels() {
     return this->alarm_control_panels_;
   }
-  alarm_control_panel::AlarmControlPanel *get_alarm_control_panel_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->alarm_control_panels_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(alarm_control_panel::AlarmControlPanel, alarm_control_panel, alarm_control_panels)
 #endif
 
 #ifdef USE_EVENT
   const std::vector &get_events() { return this->events_; }
-  event::Event *get_event_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->events_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(event::Event, event, events)
 #endif
 
 #ifdef USE_UPDATE
   const std::vector &get_updates() { return this->updates_; }
-  update::UpdateEntity *get_update_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->updates_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(update::UpdateEntity, update, updates)
 #endif
 
   Scheduler scheduler;

From b122112d58ddac936844f62637abf9eec181d135 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 15:51:17 -0500
Subject: [PATCH 270/293] Refactor API entity update dispatch to reduce code
 duplication (#9372)

---
 esphome/components/api/api_server.cpp | 150 ++++++++------------------
 1 file changed, 42 insertions(+), 108 deletions(-)

diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp
index 575229cf04..70f2ff714d 100644
--- a/esphome/components/api/api_server.cpp
+++ b/esphome/components/api/api_server.cpp
@@ -260,180 +260,114 @@ bool APIServer::check_password(const std::string &password) const {
 
 void APIServer::handle_disconnect(APIConnection *conn) {}
 
+// Macro for entities without extra parameters
+#define API_DISPATCH_UPDATE(entity_type, entity_name) \
+  void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
+    if (obj->is_internal()) \
+      return; \
+    for (auto &c : this->clients_) \
+      c->send_##entity_name##_state(obj); \
+  }
+
+// Macro for entities with extra parameters (but parameters not used in send)
+#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \
+  void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { /* NOLINT(bugprone-macro-parentheses) */ \
+    if (obj->is_internal()) \
+      return; \
+    for (auto &c : this->clients_) \
+      c->send_##entity_name##_state(obj); \
+  }
+
 #ifdef USE_BINARY_SENSOR
-void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) {
-  if (obj->is_internal())
-    return;
-  for (auto &c : this->clients_)
-    c->send_binary_sensor_state(obj);
-}
+API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor)
 #endif
 
 #ifdef USE_COVER
-void APIServer::on_cover_update(cover::Cover *obj) {
-  if (obj->is_internal())
-    return;
-  for (auto &c : this->clients_)
-    c->send_cover_state(obj);
-}
+API_DISPATCH_UPDATE(cover::Cover, cover)
 #endif
 
 #ifdef USE_FAN
-void APIServer::on_fan_update(fan::Fan *obj) {
-  if (obj->is_internal())
-    return;
-  for (auto &c : this->clients_)
-    c->send_fan_state(obj);
-}
+API_DISPATCH_UPDATE(fan::Fan, fan)
 #endif
 
 #ifdef USE_LIGHT
-void APIServer::on_light_update(light::LightState *obj) {
-  if (obj->is_internal())
-    return;
-  for (auto &c : this->clients_)
-    c->send_light_state(obj);
-}
+API_DISPATCH_UPDATE(light::LightState, light)
 #endif
 
 #ifdef USE_SENSOR
-void APIServer::on_sensor_update(sensor::Sensor *obj, float state) {
-  if (obj->is_internal())
-    return;
-  for (auto &c : this->clients_)
-    c->send_sensor_state(obj);
-}
+API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state)
 #endif
 
 #ifdef USE_SWITCH
-void APIServer::on_switch_update(switch_::Switch *obj, bool state) {
-  if (obj->is_internal())
-    return;
-  for (auto &c : this->clients_)
-    c->send_switch_state(obj);
-}
+API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state)
 #endif
 
 #ifdef USE_TEXT_SENSOR
-void APIServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) {
-  if (obj->is_internal())
-    return;
-  for (auto &c : this->clients_)
-    c->send_text_sensor_state(obj);
-}
+API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state)
 #endif
 
 #ifdef USE_CLIMATE
-void APIServer::on_climate_update(climate::Climate *obj) {
-  if (obj->is_internal())
-    return;
-  for (auto &c : this->clients_)
-    c->send_climate_state(obj);
-}
+API_DISPATCH_UPDATE(climate::Climate, climate)
 #endif
 
 #ifdef USE_NUMBER
-void APIServer::on_number_update(number::Number *obj, float state) {
-  if (obj->is_internal())
-    return;
-  for (auto &c : this->clients_)
-    c->send_number_state(obj);
-}
+API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state)
 #endif
 
 #ifdef USE_DATETIME_DATE
-void APIServer::on_date_update(datetime::DateEntity *obj) {
-  if (obj->is_internal())
-    return;
-  for (auto &c : this->clients_)
-    c->send_date_state(obj);
-}
+API_DISPATCH_UPDATE(datetime::DateEntity, date)
 #endif
 
 #ifdef USE_DATETIME_TIME
-void APIServer::on_time_update(datetime::TimeEntity *obj) {
-  if (obj->is_internal())
-    return;
-  for (auto &c : this->clients_)
-    c->send_time_state(obj);
-}
+API_DISPATCH_UPDATE(datetime::TimeEntity, time)
 #endif
 
 #ifdef USE_DATETIME_DATETIME
-void APIServer::on_datetime_update(datetime::DateTimeEntity *obj) {
-  if (obj->is_internal())
-    return;
-  for (auto &c : this->clients_)
-    c->send_datetime_state(obj);
-}
+API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime)
 #endif
 
 #ifdef USE_TEXT
-void APIServer::on_text_update(text::Text *obj, const std::string &state) {
-  if (obj->is_internal())
-    return;
-  for (auto &c : this->clients_)
-    c->send_text_state(obj);
-}
+API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state)
 #endif
 
 #ifdef USE_SELECT
-void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) {
-  if (obj->is_internal())
-    return;
-  for (auto &c : this->clients_)
-    c->send_select_state(obj);
-}
+API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index)
 #endif
 
 #ifdef USE_LOCK
-void APIServer::on_lock_update(lock::Lock *obj) {
-  if (obj->is_internal())
-    return;
-  for (auto &c : this->clients_)
-    c->send_lock_state(obj);
-}
+API_DISPATCH_UPDATE(lock::Lock, lock)
 #endif
 
 #ifdef USE_VALVE
-void APIServer::on_valve_update(valve::Valve *obj) {
-  if (obj->is_internal())
-    return;
-  for (auto &c : this->clients_)
-    c->send_valve_state(obj);
-}
+API_DISPATCH_UPDATE(valve::Valve, valve)
 #endif
 
 #ifdef USE_MEDIA_PLAYER
-void APIServer::on_media_player_update(media_player::MediaPlayer *obj) {
-  if (obj->is_internal())
-    return;
-  for (auto &c : this->clients_)
-    c->send_media_player_state(obj);
-}
+API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
 #endif
 
 #ifdef USE_EVENT
+// Event is a special case - it's the only entity that passes extra parameters to the send method
 void APIServer::on_event(event::Event *obj, const std::string &event_type) {
+  if (obj->is_internal())
+    return;
   for (auto &c : this->clients_)
     c->send_event(obj, event_type);
 }
 #endif
 
 #ifdef USE_UPDATE
+// Update is a special case - the method is called on_update, not on_update_update
 void APIServer::on_update(update::UpdateEntity *obj) {
+  if (obj->is_internal())
+    return;
   for (auto &c : this->clients_)
     c->send_update_state(obj);
 }
 #endif
 
 #ifdef USE_ALARM_CONTROL_PANEL
-void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) {
-  if (obj->is_internal())
-    return;
-  for (auto &c : this->clients_)
-    c->send_alarm_control_panel_state(obj);
-}
+API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
 #endif
 
 float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }

From 440de12e3f23c3a2e5be371fc40fc799f232634d Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 16:04:41 -0500
Subject: [PATCH 271/293] Don't compile unnecessary platform files (e.g.
 ESP8266 files on ESP32) (#9354)

---
 esphome/components/adc/__init__.py            |  26 +++-
 esphome/components/api/__init__.py            |  15 ++
 esphome/components/debug/__init__.py          |  20 +++
 esphome/components/deep_sleep/__init__.py     |  13 ++
 esphome/components/http_request/__init__.py   |  18 +++
 esphome/components/i2c/__init__.py            |  17 +++
 esphome/components/logger/__init__.py         |  24 ++++
 esphome/components/mdns/__init__.py           |  20 +++
 esphome/components/mqtt/__init__.py           |  12 ++
 esphome/components/nextion/__init__.py        |  16 +++
 esphome/components/ota/__init__.py            |  17 +++
 .../components/remote_receiver/__init__.py    |  18 +++
 .../components/remote_transmitter/__init__.py |  18 +++
 esphome/components/socket/__init__.py         |  16 +++
 esphome/components/spi/__init__.py            |  17 +++
 esphome/components/uart/__init__.py           |  18 +++
 esphome/components/wifi/__init__.py           |  17 +++
 esphome/config_helpers.py                     |  75 +++++++++-
 esphome/const.py                              |  61 ++++++--
 esphome/core/config.py                        |  15 ++
 esphome/dashboard/entries.py                  |   2 +-
 esphome/{dashboard => }/enum.py               |   0
 esphome/loader.py                             |  16 ++-
 tests/unit_tests/test_config_helpers.py       | 135 ++++++++++++++++++
 tests/unit_tests/test_loader.py               |  63 ++++++++
 25 files changed, 657 insertions(+), 12 deletions(-)
 rename esphome/{dashboard => }/enum.py (100%)
 create mode 100644 tests/unit_tests/test_config_helpers.py
 create mode 100644 tests/unit_tests/test_loader.py

diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py
index 5f94c61a08..10b7df8638 100644
--- a/esphome/components/adc/__init__.py
+++ b/esphome/components/adc/__init__.py
@@ -10,8 +10,15 @@ from esphome.components.esp32.const import (
     VARIANT_ESP32S2,
     VARIANT_ESP32S3,
 )
+from esphome.config_helpers import filter_source_files_from_platform
 import esphome.config_validation as cv
-from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
+from esphome.const import (
+    CONF_ANALOG,
+    CONF_INPUT,
+    CONF_NUMBER,
+    PLATFORM_ESP8266,
+    PlatformFramework,
+)
 from esphome.core import CORE
 
 CODEOWNERS = ["@esphome/core"]
@@ -229,3 +236,20 @@ def validate_adc_pin(value):
         )(value)
 
     raise NotImplementedError
+
+
+FILTER_SOURCE_FILES = filter_source_files_from_platform(
+    {
+        "adc_sensor_esp32.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+        },
+        "adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
+        "adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
+        "adc_sensor_libretiny.cpp": {
+            PlatformFramework.BK72XX_ARDUINO,
+            PlatformFramework.RTL87XX_ARDUINO,
+            PlatformFramework.LN882X_ARDUINO,
+        },
+    }
+)
diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py
index 2f1be28293..eb8883b025 100644
--- a/esphome/components/api/__init__.py
+++ b/esphome/components/api/__init__.py
@@ -3,6 +3,7 @@ import base64
 from esphome import automation
 from esphome.automation import Condition
 import esphome.codegen as cg
+from esphome.config_helpers import get_logger_level
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_ACTION,
@@ -313,3 +314,17 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
 @automation.register_condition("api.connected", APIConnectedCondition, {})
 async def api_connected_to_code(config, condition_id, template_arg, args):
     return cg.new_Pvariable(condition_id, template_arg)
+
+
+def FILTER_SOURCE_FILES() -> list[str]:
+    """Filter out api_pb2_dump.cpp when proto message dumping is not enabled."""
+    # api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined
+    # This is a particularly large file that still needs to be opened and read
+    # all the way to the end even when ifdef'd out
+    #
+    # HAS_PROTO_MESSAGE_DUMP is defined when ESPHOME_LOG_HAS_VERY_VERBOSE is set,
+    # which happens when the logger level is VERY_VERBOSE
+    if get_logger_level() != "VERY_VERBOSE":
+        return ["api_pb2_dump.cpp"]
+
+    return []
diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py
index 1955b5d22c..500dfac1fe 100644
--- a/esphome/components/debug/__init__.py
+++ b/esphome/components/debug/__init__.py
@@ -1,4 +1,5 @@
 import esphome.codegen as cg
+from esphome.config_helpers import filter_source_files_from_platform
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_BLOCK,
@@ -7,6 +8,7 @@ from esphome.const import (
     CONF_FREE,
     CONF_ID,
     CONF_LOOP_TIME,
+    PlatformFramework,
 )
 
 CODEOWNERS = ["@OttoWinter"]
@@ -44,3 +46,21 @@ CONFIG_SCHEMA = cv.All(
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     await cg.register_component(var, config)
+
+
+FILTER_SOURCE_FILES = filter_source_files_from_platform(
+    {
+        "debug_esp32.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+        },
+        "debug_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
+        "debug_host.cpp": {PlatformFramework.HOST_NATIVE},
+        "debug_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
+        "debug_libretiny.cpp": {
+            PlatformFramework.BK72XX_ARDUINO,
+            PlatformFramework.RTL87XX_ARDUINO,
+            PlatformFramework.LN882X_ARDUINO,
+        },
+    }
+)
diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py
index 63b359bd5b..55826f52bb 100644
--- a/esphome/components/deep_sleep/__init__.py
+++ b/esphome/components/deep_sleep/__init__.py
@@ -11,6 +11,7 @@ from esphome.components.esp32.const import (
     VARIANT_ESP32S2,
     VARIANT_ESP32S3,
 )
+from esphome.config_helpers import filter_source_files_from_platform
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_DEFAULT,
@@ -27,6 +28,7 @@ from esphome.const import (
     CONF_WAKEUP_PIN,
     PLATFORM_ESP32,
     PLATFORM_ESP8266,
+    PlatformFramework,
 )
 
 WAKEUP_PINS = {
@@ -313,3 +315,14 @@ async def deep_sleep_action_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
     await cg.register_parented(var, config[CONF_ID])
     return var
+
+
+FILTER_SOURCE_FILES = filter_source_files_from_platform(
+    {
+        "deep_sleep_esp32.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+        },
+        "deep_sleep_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
+    }
+)
diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py
index 18373edb77..0d32bc97c2 100644
--- a/esphome/components/http_request/__init__.py
+++ b/esphome/components/http_request/__init__.py
@@ -2,6 +2,7 @@ from esphome import automation
 import esphome.codegen as cg
 from esphome.components import esp32
 from esphome.components.const import CONF_REQUEST_HEADERS
+from esphome.config_helpers import filter_source_files_from_platform
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_ESP8266_DISABLE_SSL_SUPPORT,
@@ -13,6 +14,7 @@ from esphome.const import (
     CONF_URL,
     CONF_WATCHDOG_TIMEOUT,
     PLATFORM_HOST,
+    PlatformFramework,
     __version__,
 )
 from esphome.core import CORE, Lambda
@@ -319,3 +321,19 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
         await automation.build_automation(trigger, [], conf)
 
     return var
+
+
+FILTER_SOURCE_FILES = filter_source_files_from_platform(
+    {
+        "http_request_host.cpp": {PlatformFramework.HOST_NATIVE},
+        "http_request_arduino.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP8266_ARDUINO,
+            PlatformFramework.RP2040_ARDUINO,
+            PlatformFramework.BK72XX_ARDUINO,
+            PlatformFramework.RTL87XX_ARDUINO,
+            PlatformFramework.LN882X_ARDUINO,
+        },
+        "http_request_idf.cpp": {PlatformFramework.ESP32_IDF},
+    }
+)
diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py
index 6adb9b71aa..4172b23845 100644
--- a/esphome/components/i2c/__init__.py
+++ b/esphome/components/i2c/__init__.py
@@ -3,6 +3,7 @@ import logging
 from esphome import pins
 import esphome.codegen as cg
 from esphome.components import esp32
+from esphome.config_helpers import filter_source_files_from_platform
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_ADDRESS,
@@ -18,6 +19,7 @@ from esphome.const import (
     PLATFORM_ESP32,
     PLATFORM_ESP8266,
     PLATFORM_RP2040,
+    PlatformFramework,
 )
 from esphome.core import CORE, coroutine_with_priority
 import esphome.final_validate as fv
@@ -205,3 +207,18 @@ def final_validate_device_schema(
         {cv.Required(CONF_I2C_ID): fv.id_declaration_match_schema(hub_schema)},
         extra=cv.ALLOW_EXTRA,
     )
+
+
+FILTER_SOURCE_FILES = filter_source_files_from_platform(
+    {
+        "i2c_bus_arduino.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP8266_ARDUINO,
+            PlatformFramework.RP2040_ARDUINO,
+            PlatformFramework.BK72XX_ARDUINO,
+            PlatformFramework.RTL87XX_ARDUINO,
+            PlatformFramework.LN882X_ARDUINO,
+        },
+        "i2c_bus_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
+    }
+)
diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py
index 3d4907aa6e..9ac2999696 100644
--- a/esphome/components/logger/__init__.py
+++ b/esphome/components/logger/__init__.py
@@ -21,6 +21,7 @@ from esphome.components.libretiny.const import (
     COMPONENT_LN882X,
     COMPONENT_RTL87XX,
 )
+from esphome.config_helpers import filter_source_files_from_platform
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_ARGS,
@@ -42,6 +43,7 @@ from esphome.const import (
     PLATFORM_LN882X,
     PLATFORM_RP2040,
     PLATFORM_RTL87XX,
+    PlatformFramework,
 )
 from esphome.core import CORE, Lambda, coroutine_with_priority
 
@@ -444,3 +446,25 @@ async def logger_set_level_to_code(config, action_id, template_arg, args):
 
     lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void)
     return cg.new_Pvariable(action_id, template_arg, lambda_)
+
+
+FILTER_SOURCE_FILES = filter_source_files_from_platform(
+    {
+        "logger_esp32.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+        },
+        "logger_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
+        "logger_host.cpp": {PlatformFramework.HOST_NATIVE},
+        "logger_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
+        "logger_libretiny.cpp": {
+            PlatformFramework.BK72XX_ARDUINO,
+            PlatformFramework.RTL87XX_ARDUINO,
+            PlatformFramework.LN882X_ARDUINO,
+        },
+        "task_log_buffer.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+        },
+    }
+)
diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py
index ed230d43aa..e32d39cede 100644
--- a/esphome/components/mdns/__init__.py
+++ b/esphome/components/mdns/__init__.py
@@ -1,5 +1,6 @@
 import esphome.codegen as cg
 from esphome.components.esp32 import add_idf_component
+from esphome.config_helpers import filter_source_files_from_platform
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_DISABLED,
@@ -8,6 +9,7 @@ from esphome.const import (
     CONF_PROTOCOL,
     CONF_SERVICE,
     CONF_SERVICES,
+    PlatformFramework,
 )
 from esphome.core import CORE, coroutine_with_priority
 
@@ -108,3 +110,21 @@ async def to_code(config):
         )
 
         cg.add(var.add_extra_service(exp))
+
+
+FILTER_SOURCE_FILES = filter_source_files_from_platform(
+    {
+        "mdns_esp32.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+        },
+        "mdns_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
+        "mdns_host.cpp": {PlatformFramework.HOST_NATIVE},
+        "mdns_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
+        "mdns_libretiny.cpp": {
+            PlatformFramework.BK72XX_ARDUINO,
+            PlatformFramework.RTL87XX_ARDUINO,
+            PlatformFramework.LN882X_ARDUINO,
+        },
+    }
+)
diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py
index f0d5a95d43..1a6fcabf42 100644
--- a/esphome/components/mqtt/__init__.py
+++ b/esphome/components/mqtt/__init__.py
@@ -5,6 +5,7 @@ from esphome.automation import Condition
 import esphome.codegen as cg
 from esphome.components import logger
 from esphome.components.esp32 import add_idf_sdkconfig_option
+from esphome.config_helpers import filter_source_files_from_platform
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_AVAILABILITY,
@@ -54,6 +55,7 @@ from esphome.const import (
     PLATFORM_BK72XX,
     PLATFORM_ESP32,
     PLATFORM_ESP8266,
+    PlatformFramework,
 )
 from esphome.core import CORE, coroutine_with_priority
 
@@ -596,3 +598,13 @@ async def mqtt_enable_to_code(config, action_id, template_arg, args):
 async def mqtt_disable_to_code(config, action_id, template_arg, args):
     paren = await cg.get_variable(config[CONF_ID])
     return cg.new_Pvariable(action_id, template_arg, paren)
+
+
+FILTER_SOURCE_FILES = filter_source_files_from_platform(
+    {
+        "mqtt_backend_esp32.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+        },
+    }
+)
diff --git a/esphome/components/nextion/__init__.py b/esphome/components/nextion/__init__.py
index fb75daf4ba..8adc49d68c 100644
--- a/esphome/components/nextion/__init__.py
+++ b/esphome/components/nextion/__init__.py
@@ -1,5 +1,7 @@
 import esphome.codegen as cg
 from esphome.components import uart
+from esphome.config_helpers import filter_source_files_from_platform
+from esphome.const import PlatformFramework
 
 nextion_ns = cg.esphome_ns.namespace("nextion")
 Nextion = nextion_ns.class_("Nextion", cg.PollingComponent, uart.UARTDevice)
@@ -8,3 +10,17 @@ nextion_ref = Nextion.operator("ref")
 CONF_NEXTION_ID = "nextion_id"
 CONF_PUBLISH_STATE = "publish_state"
 CONF_SEND_TO_NEXTION = "send_to_nextion"
+
+FILTER_SOURCE_FILES = filter_source_files_from_platform(
+    {
+        "nextion_upload_arduino.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP8266_ARDUINO,
+            PlatformFramework.RP2040_ARDUINO,
+            PlatformFramework.BK72XX_ARDUINO,
+            PlatformFramework.RTL87XX_ARDUINO,
+            PlatformFramework.LN882X_ARDUINO,
+        },
+        "nextion_upload_idf.cpp": {PlatformFramework.ESP32_IDF},
+    }
+)
diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py
index 627c55e910..4d5b8a61e2 100644
--- a/esphome/components/ota/__init__.py
+++ b/esphome/components/ota/__init__.py
@@ -1,5 +1,6 @@
 from esphome import automation
 import esphome.codegen as cg
+from esphome.config_helpers import filter_source_files_from_platform
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_ESPHOME,
@@ -7,6 +8,7 @@ from esphome.const import (
     CONF_OTA,
     CONF_PLATFORM,
     CONF_TRIGGER_ID,
+    PlatformFramework,
 )
 from esphome.core import CORE, coroutine_with_priority
 
@@ -120,3 +122,18 @@ async def ota_to_code(var, config):
         use_state_callback = True
     if use_state_callback:
         cg.add_define("USE_OTA_STATE_CALLBACK")
+
+
+FILTER_SOURCE_FILES = filter_source_files_from_platform(
+    {
+        "ota_backend_arduino_esp32.cpp": {PlatformFramework.ESP32_ARDUINO},
+        "ota_backend_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
+        "ota_backend_arduino_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
+        "ota_backend_arduino_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
+        "ota_backend_arduino_libretiny.cpp": {
+            PlatformFramework.BK72XX_ARDUINO,
+            PlatformFramework.RTL87XX_ARDUINO,
+            PlatformFramework.LN882X_ARDUINO,
+        },
+    }
+)
diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py
index 5de7d8c9c4..dffc088085 100644
--- a/esphome/components/remote_receiver/__init__.py
+++ b/esphome/components/remote_receiver/__init__.py
@@ -1,6 +1,7 @@
 from esphome import pins
 import esphome.codegen as cg
 from esphome.components import esp32, esp32_rmt, remote_base
+from esphome.config_helpers import filter_source_files_from_platform
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_BUFFER_SIZE,
@@ -15,6 +16,7 @@ from esphome.const import (
     CONF_TYPE,
     CONF_USE_DMA,
     CONF_VALUE,
+    PlatformFramework,
 )
 from esphome.core import CORE, TimePeriod
 
@@ -170,3 +172,19 @@ async def to_code(config):
     cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
     cg.add(var.set_filter_us(config[CONF_FILTER]))
     cg.add(var.set_idle_us(config[CONF_IDLE]))
+
+
+FILTER_SOURCE_FILES = filter_source_files_from_platform(
+    {
+        "remote_receiver_esp32.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+        },
+        "remote_receiver_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
+        "remote_receiver_libretiny.cpp": {
+            PlatformFramework.BK72XX_ARDUINO,
+            PlatformFramework.RTL87XX_ARDUINO,
+            PlatformFramework.LN882X_ARDUINO,
+        },
+    }
+)
diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py
index 713cee0186..47a46ff56b 100644
--- a/esphome/components/remote_transmitter/__init__.py
+++ b/esphome/components/remote_transmitter/__init__.py
@@ -1,6 +1,7 @@
 from esphome import automation, pins
 import esphome.codegen as cg
 from esphome.components import esp32, esp32_rmt, remote_base
+from esphome.config_helpers import filter_source_files_from_platform
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_CARRIER_DUTY_PERCENT,
@@ -12,6 +13,7 @@ from esphome.const import (
     CONF_PIN,
     CONF_RMT_SYMBOLS,
     CONF_USE_DMA,
+    PlatformFramework,
 )
 from esphome.core import CORE
 
@@ -95,3 +97,19 @@ async def to_code(config):
         await automation.build_automation(
             var.get_complete_trigger(), [], on_complete_config
         )
+
+
+FILTER_SOURCE_FILES = filter_source_files_from_platform(
+    {
+        "remote_transmitter_esp32.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+        },
+        "remote_transmitter_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
+        "remote_transmitter_libretiny.cpp": {
+            PlatformFramework.BK72XX_ARDUINO,
+            PlatformFramework.RTL87XX_ARDUINO,
+            PlatformFramework.LN882X_ARDUINO,
+        },
+    }
+)
diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py
index 26031a8da5..e085a09eac 100644
--- a/esphome/components/socket/__init__.py
+++ b/esphome/components/socket/__init__.py
@@ -1,5 +1,6 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
+from esphome.core import CORE
 
 CODEOWNERS = ["@esphome/core"]
 
@@ -40,3 +41,18 @@ async def to_code(config):
     elif impl == IMPLEMENTATION_BSD_SOCKETS:
         cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS")
         cg.add_define("USE_SOCKET_SELECT_SUPPORT")
+
+
+def FILTER_SOURCE_FILES() -> list[str]:
+    """Return list of socket implementation files that aren't selected by the user."""
+    impl = CORE.config["socket"][CONF_IMPLEMENTATION]
+
+    # Build list of files to exclude based on selected implementation
+    excluded = []
+    if impl != IMPLEMENTATION_LWIP_TCP:
+        excluded.append("lwip_raw_tcp_impl.cpp")
+    if impl != IMPLEMENTATION_BSD_SOCKETS:
+        excluded.append("bsd_sockets_impl.cpp")
+    if impl != IMPLEMENTATION_LWIP_SOCKETS:
+        excluded.append("lwip_sockets_impl.cpp")
+    return excluded
diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py
index 55a4b9c8f6..58bfc3f411 100644
--- a/esphome/components/spi/__init__.py
+++ b/esphome/components/spi/__init__.py
@@ -13,6 +13,7 @@ from esphome.components.esp32.const import (
     VARIANT_ESP32S2,
     VARIANT_ESP32S3,
 )
+from esphome.config_helpers import filter_source_files_from_platform
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_CLK_PIN,
@@ -31,6 +32,7 @@ from esphome.const import (
     PLATFORM_ESP32,
     PLATFORM_ESP8266,
     PLATFORM_RP2040,
+    PlatformFramework,
 )
 from esphome.core import CORE, coroutine_with_priority
 import esphome.final_validate as fv
@@ -423,3 +425,18 @@ def final_validate_device_schema(name: str, *, require_mosi: bool, require_miso:
         {cv.Required(CONF_SPI_ID): fv.id_declaration_match_schema(hub_schema)},
         extra=cv.ALLOW_EXTRA,
     )
+
+
+FILTER_SOURCE_FILES = filter_source_files_from_platform(
+    {
+        "spi_arduino.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP8266_ARDUINO,
+            PlatformFramework.RP2040_ARDUINO,
+            PlatformFramework.BK72XX_ARDUINO,
+            PlatformFramework.RTL87XX_ARDUINO,
+            PlatformFramework.LN882X_ARDUINO,
+        },
+        "spi_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
+    }
+)
diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py
index a0908a299c..7d4c6360fe 100644
--- a/esphome/components/uart/__init__.py
+++ b/esphome/components/uart/__init__.py
@@ -2,6 +2,7 @@ import re
 
 from esphome import automation, pins
 import esphome.codegen as cg
+from esphome.config_helpers import filter_source_files_from_platform
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_AFTER,
@@ -27,6 +28,7 @@ from esphome.const import (
     CONF_TX_PIN,
     CONF_UART_ID,
     PLATFORM_HOST,
+    PlatformFramework,
 )
 from esphome.core import CORE
 import esphome.final_validate as fv
@@ -438,3 +440,19 @@ async def uart_write_to_code(config, action_id, template_arg, args):
     else:
         cg.add(var.set_data_static(data))
     return var
+
+
+FILTER_SOURCE_FILES = filter_source_files_from_platform(
+    {
+        "uart_component_esp32_arduino.cpp": {PlatformFramework.ESP32_ARDUINO},
+        "uart_component_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
+        "uart_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
+        "uart_component_host.cpp": {PlatformFramework.HOST_NATIVE},
+        "uart_component_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
+        "uart_component_libretiny.cpp": {
+            PlatformFramework.BK72XX_ARDUINO,
+            PlatformFramework.RTL87XX_ARDUINO,
+            PlatformFramework.LN882X_ARDUINO,
+        },
+    }
+)
diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py
index e8ae9b1b4e..61f37556ba 100644
--- a/esphome/components/wifi/__init__.py
+++ b/esphome/components/wifi/__init__.py
@@ -3,6 +3,7 @@ from esphome.automation import Condition
 import esphome.codegen as cg
 from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
 from esphome.components.network import IPAddress
+from esphome.config_helpers import filter_source_files_from_platform
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_AP,
@@ -39,6 +40,7 @@ from esphome.const import (
     CONF_TTLS_PHASE_2,
     CONF_USE_ADDRESS,
     CONF_USERNAME,
+    PlatformFramework,
 )
 from esphome.core import CORE, HexInt, coroutine_with_priority
 import esphome.final_validate as fv
@@ -526,3 +528,18 @@ async def wifi_set_sta_to_code(config, action_id, template_arg, args):
         await automation.build_automation(var.get_error_trigger(), [], on_error_config)
     await cg.register_component(var, config)
     return var
+
+
+FILTER_SOURCE_FILES = filter_source_files_from_platform(
+    {
+        "wifi_component_esp32_arduino.cpp": {PlatformFramework.ESP32_ARDUINO},
+        "wifi_component_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
+        "wifi_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
+        "wifi_component_libretiny.cpp": {
+            PlatformFramework.BK72XX_ARDUINO,
+            PlatformFramework.RTL87XX_ARDUINO,
+            PlatformFramework.LN882X_ARDUINO,
+        },
+        "wifi_component_pico_w.cpp": {PlatformFramework.RP2040_ARDUINO},
+    }
+)
diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py
index 54242bc259..50ce4e8e34 100644
--- a/esphome/config_helpers.py
+++ b/esphome/config_helpers.py
@@ -1,4 +1,20 @@
-from esphome.const import CONF_ID
+from collections.abc import Callable
+
+from esphome.const import (
+    CONF_ID,
+    CONF_LEVEL,
+    CONF_LOGGER,
+    KEY_CORE,
+    KEY_TARGET_FRAMEWORK,
+    KEY_TARGET_PLATFORM,
+    PlatformFramework,
+)
+from esphome.core import CORE
+
+# Pre-build lookup map from (platform, framework) tuples to PlatformFramework enum
+_PLATFORM_FRAMEWORK_LOOKUP = {
+    (pf.value[0].value, pf.value[1].value): pf for pf in PlatformFramework
+}
 
 
 class Extend:
@@ -103,3 +119,60 @@ def merge_config(full_old, full_new):
         return new
 
     return merge(full_old, full_new)
+
+
+def filter_source_files_from_platform(
+    files_map: dict[str, set[PlatformFramework]],
+) -> Callable[[], list[str]]:
+    """Helper to build a FILTER_SOURCE_FILES function from platform mapping.
+
+    Args:
+        files_map: Dict mapping filename to set of PlatformFramework enums
+                  that should compile this file
+
+    Returns:
+        Function that returns list of files to exclude for current platform
+    """
+
+    def filter_source_files() -> list[str]:
+        # Get current platform/framework
+        core_data = CORE.data.get(KEY_CORE, {})
+        target_platform = core_data.get(KEY_TARGET_PLATFORM)
+        target_framework = core_data.get(KEY_TARGET_FRAMEWORK)
+
+        if not target_platform or not target_framework:
+            return []
+
+        # Direct lookup of current PlatformFramework
+        current_platform_framework = _PLATFORM_FRAMEWORK_LOOKUP.get(
+            (target_platform, target_framework)
+        )
+
+        if not current_platform_framework:
+            return []
+
+        # Return files that should be excluded for current platform
+        return [
+            filename
+            for filename, platforms in files_map.items()
+            if current_platform_framework not in platforms
+        ]
+
+    return filter_source_files
+
+
+def get_logger_level() -> str:
+    """Get the configured logger level.
+
+    This is used by components to determine what logging features to include
+    based on the configured log level.
+
+    Returns:
+        The configured logger level string, defaults to "DEBUG" if not configured
+    """
+    # Check if logger config exists
+    if CONF_LOGGER not in CORE.config:
+        return "DEBUG"
+
+    logger_config = CORE.config[CONF_LOGGER]
+    return logger_config.get(CONF_LEVEL, "DEBUG")
diff --git a/esphome/const.py b/esphome/const.py
index 4aeb5179e6..085b9b39b8 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,5 +1,9 @@
 """Constants used by esphome."""
 
+from enum import Enum
+
+from esphome.enum import StrEnum
+
 __version__ = "2025.7.0-dev"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
@@ -7,14 +11,55 @@ VALID_SUBSTITUTIONS_CHARACTERS = (
     "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
 )
 
-PLATFORM_BK72XX = "bk72xx"
-PLATFORM_ESP32 = "esp32"
-PLATFORM_ESP8266 = "esp8266"
-PLATFORM_HOST = "host"
-PLATFORM_LIBRETINY_OLDSTYLE = "libretiny"
-PLATFORM_LN882X = "ln882x"
-PLATFORM_RP2040 = "rp2040"
-PLATFORM_RTL87XX = "rtl87xx"
+
+class Platform(StrEnum):
+    """Platform identifiers for ESPHome."""
+
+    BK72XX = "bk72xx"
+    ESP32 = "esp32"
+    ESP8266 = "esp8266"
+    HOST = "host"
+    LIBRETINY_OLDSTYLE = "libretiny"
+    LN882X = "ln882x"
+    RP2040 = "rp2040"
+    RTL87XX = "rtl87xx"
+
+
+class Framework(StrEnum):
+    """Framework identifiers for ESPHome."""
+
+    ARDUINO = "arduino"
+    ESP_IDF = "esp-idf"
+    NATIVE = "host"
+
+
+class PlatformFramework(Enum):
+    """Combined platform-framework identifiers with tuple values."""
+
+    # ESP32 variants
+    ESP32_ARDUINO = (Platform.ESP32, Framework.ARDUINO)
+    ESP32_IDF = (Platform.ESP32, Framework.ESP_IDF)
+
+    # Arduino framework platforms
+    ESP8266_ARDUINO = (Platform.ESP8266, Framework.ARDUINO)
+    RP2040_ARDUINO = (Platform.RP2040, Framework.ARDUINO)
+    BK72XX_ARDUINO = (Platform.BK72XX, Framework.ARDUINO)
+    RTL87XX_ARDUINO = (Platform.RTL87XX, Framework.ARDUINO)
+    LN882X_ARDUINO = (Platform.LN882X, Framework.ARDUINO)
+
+    # Host platform (native)
+    HOST_NATIVE = (Platform.HOST, Framework.NATIVE)
+
+
+# Maintain backward compatibility by reassigning after enum definition
+PLATFORM_BK72XX = Platform.BK72XX
+PLATFORM_ESP32 = Platform.ESP32
+PLATFORM_ESP8266 = Platform.ESP8266
+PLATFORM_HOST = Platform.HOST
+PLATFORM_LIBRETINY_OLDSTYLE = Platform.LIBRETINY_OLDSTYLE
+PLATFORM_LN882X = Platform.LN882X
+PLATFORM_RP2040 = Platform.RP2040
+PLATFORM_RTL87XX = Platform.RTL87XX
 
 
 SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"}
diff --git a/esphome/core/config.py b/esphome/core/config.py
index 641c73a292..f73369f28f 100644
--- a/esphome/core/config.py
+++ b/esphome/core/config.py
@@ -6,6 +6,7 @@ from pathlib import Path
 
 from esphome import automation, core
 import esphome.codegen as cg
+from esphome.config_helpers import filter_source_files_from_platform
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_AREA,
@@ -35,6 +36,7 @@ from esphome.const import (
     CONF_TRIGGER_ID,
     CONF_VERSION,
     KEY_CORE,
+    PlatformFramework,
     __version__ as ESPHOME_VERSION,
 )
 from esphome.core import CORE, coroutine_with_priority
@@ -551,3 +553,16 @@ async def to_code(config: ConfigType) -> None:
             cg.add(dev.set_area_id(area_id_hash))
 
         cg.add(cg.App.register_device(dev))
+
+
+# Platform-specific source files for core
+FILTER_SOURCE_FILES = filter_source_files_from_platform(
+    {
+        "ring_buffer.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+        },
+        # Note: lock_free_queue.h and event_pool.h are header files and don't need to be filtered
+        # as they are only included when needed by the preprocessor
+    }
+)
diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py
index e4825298f7..b138cfd272 100644
--- a/esphome/dashboard/entries.py
+++ b/esphome/dashboard/entries.py
@@ -9,6 +9,7 @@ import os
 from typing import TYPE_CHECKING, Any
 
 from esphome import const, util
+from esphome.enum import StrEnum
 from esphome.storage_json import StorageJSON, ext_storage_path
 
 from .const import (
@@ -18,7 +19,6 @@ from .const import (
     EVENT_ENTRY_STATE_CHANGED,
     EVENT_ENTRY_UPDATED,
 )
-from .enum import StrEnum
 from .util.subprocess import async_run_system_command
 
 if TYPE_CHECKING:
diff --git a/esphome/dashboard/enum.py b/esphome/enum.py
similarity index 100%
rename from esphome/dashboard/enum.py
rename to esphome/enum.py
diff --git a/esphome/loader.py b/esphome/loader.py
index 79a1d7f576..7b2472521a 100644
--- a/esphome/loader.py
+++ b/esphome/loader.py
@@ -112,8 +112,17 @@ class ComponentManifest:
         This will return all cpp source files that are located in the same folder as the
         loaded .py file (does not look through subdirectories)
         """
-        ret = []
+        ret: list[FileResource] = []
 
+        # Get filter function for source files
+        filter_source_files_func = getattr(self.module, "FILTER_SOURCE_FILES", None)
+
+        # Get list of files to exclude
+        excluded_files = (
+            set(filter_source_files_func()) if filter_source_files_func else set()
+        )
+
+        # Process all resources
         for resource in (
             r.name
             for r in importlib.resources.files(self.package).iterdir()
@@ -124,6 +133,11 @@ class ComponentManifest:
             if not importlib.resources.files(self.package).joinpath(resource).is_file():
                 # Not a resource = this is a directory (yeah this is confusing)
                 continue
+
+            # Skip excluded files
+            if resource in excluded_files:
+                continue
+
             ret.append(FileResource(self.package, resource))
         return ret
 
diff --git a/tests/unit_tests/test_config_helpers.py b/tests/unit_tests/test_config_helpers.py
new file mode 100644
index 0000000000..1c850e3759
--- /dev/null
+++ b/tests/unit_tests/test_config_helpers.py
@@ -0,0 +1,135 @@
+"""Unit tests for esphome.config_helpers module."""
+
+from collections.abc import Callable
+from unittest.mock import patch
+
+from esphome.config_helpers import filter_source_files_from_platform, get_logger_level
+from esphome.const import (
+    CONF_LEVEL,
+    CONF_LOGGER,
+    KEY_CORE,
+    KEY_TARGET_FRAMEWORK,
+    KEY_TARGET_PLATFORM,
+    PlatformFramework,
+)
+
+
+def test_filter_source_files_from_platform_esp32() -> None:
+    """Test that filter_source_files_from_platform correctly filters files for ESP32 platform."""
+    # Define test file mappings
+    files_map: dict[str, set[PlatformFramework]] = {
+        "logger_esp32.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+        },
+        "logger_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
+        "logger_host.cpp": {PlatformFramework.HOST_NATIVE},
+        "logger_common.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+            PlatformFramework.ESP8266_ARDUINO,
+            PlatformFramework.HOST_NATIVE,
+        },
+    }
+
+    # Create the filter function
+    filter_func: Callable[[], list[str]] = filter_source_files_from_platform(files_map)
+
+    # Test ESP32 with Arduino framework
+    mock_core_data: dict[str, dict[str, str]] = {
+        KEY_CORE: {
+            KEY_TARGET_PLATFORM: "esp32",
+            KEY_TARGET_FRAMEWORK: "arduino",
+        }
+    }
+
+    with patch("esphome.config_helpers.CORE.data", mock_core_data):
+        excluded: list[str] = filter_func()
+        # ESP32 Arduino should exclude ESP8266 and HOST files
+        assert "logger_esp8266.cpp" in excluded
+        assert "logger_host.cpp" in excluded
+        # But not ESP32 or common files
+        assert "logger_esp32.cpp" not in excluded
+        assert "logger_common.cpp" not in excluded
+
+
+def test_filter_source_files_from_platform_host() -> None:
+    """Test that filter_source_files_from_platform correctly filters files for HOST platform."""
+    # Define test file mappings
+    files_map: dict[str, set[PlatformFramework]] = {
+        "logger_esp32.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+        },
+        "logger_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
+        "logger_host.cpp": {PlatformFramework.HOST_NATIVE},
+        "logger_common.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+            PlatformFramework.ESP8266_ARDUINO,
+            PlatformFramework.HOST_NATIVE,
+        },
+    }
+
+    # Create the filter function
+    filter_func: Callable[[], list[str]] = filter_source_files_from_platform(files_map)
+
+    # Test Host platform
+    mock_core_data: dict[str, dict[str, str]] = {
+        KEY_CORE: {
+            KEY_TARGET_PLATFORM: "host",
+            KEY_TARGET_FRAMEWORK: "host",  # Framework.NATIVE is "host"
+        }
+    }
+
+    with patch("esphome.config_helpers.CORE.data", mock_core_data):
+        excluded: list[str] = filter_func()
+        # Host should exclude ESP32 and ESP8266 files
+        assert "logger_esp32.cpp" in excluded
+        assert "logger_esp8266.cpp" in excluded
+        # But not host or common files
+        assert "logger_host.cpp" not in excluded
+        assert "logger_common.cpp" not in excluded
+
+
+def test_filter_source_files_from_platform_handles_missing_data() -> None:
+    """Test that filter_source_files_from_platform returns empty list when platform/framework data is missing."""
+    # Define test file mappings
+    files_map: dict[str, set[PlatformFramework]] = {
+        "logger_esp32.cpp": {PlatformFramework.ESP32_ARDUINO},
+        "logger_host.cpp": {PlatformFramework.HOST_NATIVE},
+    }
+
+    # Create the filter function
+    filter_func: Callable[[], list[str]] = filter_source_files_from_platform(files_map)
+
+    # Test case: Missing platform/framework data
+    mock_core_data: dict[str, dict[str, str]] = {KEY_CORE: {}}
+
+    with patch("esphome.config_helpers.CORE.data", mock_core_data):
+        excluded: list[str] = filter_func()
+        # Should return empty list when platform/framework not set
+        assert excluded == []
+
+
+def test_get_logger_level() -> None:
+    """Test get_logger_level helper function."""
+    # Test no logger config - should return default DEBUG
+    mock_config = {}
+    with patch("esphome.config_helpers.CORE.config", mock_config):
+        assert get_logger_level() == "DEBUG"
+
+    # Test with logger set to INFO
+    mock_config = {CONF_LOGGER: {CONF_LEVEL: "INFO"}}
+    with patch("esphome.config_helpers.CORE.config", mock_config):
+        assert get_logger_level() == "INFO"
+
+    # Test with VERY_VERBOSE
+    mock_config = {CONF_LOGGER: {CONF_LEVEL: "VERY_VERBOSE"}}
+    with patch("esphome.config_helpers.CORE.config", mock_config):
+        assert get_logger_level() == "VERY_VERBOSE"
+
+    # Test with logger missing level (uses default DEBUG)
+    mock_config = {CONF_LOGGER: {}}
+    with patch("esphome.config_helpers.CORE.config", mock_config):
+        assert get_logger_level() == "DEBUG"
diff --git a/tests/unit_tests/test_loader.py b/tests/unit_tests/test_loader.py
new file mode 100644
index 0000000000..c6d4c4aef0
--- /dev/null
+++ b/tests/unit_tests/test_loader.py
@@ -0,0 +1,63 @@
+"""Unit tests for esphome.loader module."""
+
+from unittest.mock import MagicMock, patch
+
+from esphome.loader import ComponentManifest
+
+
+def test_component_manifest_resources_with_filter_source_files() -> None:
+    """Test that ComponentManifest.resources correctly filters out excluded files."""
+    # Create a mock module with FILTER_SOURCE_FILES function
+    mock_module = MagicMock()
+    mock_module.FILTER_SOURCE_FILES = lambda: [
+        "platform_esp32.cpp",
+        "platform_esp8266.cpp",
+    ]
+    mock_module.__package__ = "esphome.components.test_component"
+
+    # Create ComponentManifest instance
+    manifest = ComponentManifest(mock_module)
+
+    # Mock the files in the package
+    def create_mock_file(filename: str) -> MagicMock:
+        mock_file = MagicMock()
+        mock_file.name = filename
+        mock_file.is_file.return_value = True
+        return mock_file
+
+    mock_files = [
+        create_mock_file("test.cpp"),
+        create_mock_file("test.h"),
+        create_mock_file("platform_esp32.cpp"),
+        create_mock_file("platform_esp8266.cpp"),
+        create_mock_file("common.cpp"),
+        create_mock_file("README.md"),  # Should be excluded by extension
+    ]
+
+    # Mock importlib.resources
+    with patch("importlib.resources.files") as mock_files_func:
+        mock_package_files = MagicMock()
+        mock_package_files.iterdir.return_value = mock_files
+        mock_package_files.joinpath = lambda name: MagicMock(is_file=lambda: True)
+        mock_files_func.return_value = mock_package_files
+
+        # Get resources
+        resources = manifest.resources
+
+        # Convert to list of filenames for easier testing
+        resource_names = [r.resource for r in resources]
+
+        # Check that platform files are excluded
+        assert "platform_esp32.cpp" not in resource_names
+        assert "platform_esp8266.cpp" not in resource_names
+
+        # Check that other source files are included
+        assert "test.cpp" in resource_names
+        assert "test.h" in resource_names
+        assert "common.cpp" in resource_names
+
+        # Check that non-source files are excluded
+        assert "README.md" not in resource_names
+
+        # Verify the correct number of resources
+        assert len(resources) == 3  # test.cpp, test.h, common.cpp

From 206659ddb82d1a95a29d38b78f76200b3a8c9204 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 16:15:49 -0500
Subject: [PATCH 272/293] Refactor voice assistant API methods to reduce code
 duplication (#9374)

---
 esphome/components/api/api_connection.cpp | 103 +++++++++-------------
 esphome/components/api/api_connection.h   |   5 ++
 2 files changed, 47 insertions(+), 61 deletions(-)

diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp
index 13c5b345b6..01f4552842 100644
--- a/esphome/components/api/api_connection.cpp
+++ b/esphome/components/api/api_connection.cpp
@@ -1176,66 +1176,53 @@ void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequ
 #endif
 
 #ifdef USE_VOICE_ASSISTANT
+bool APIConnection::check_voice_assistant_api_connection_() const {
+  return voice_assistant::global_voice_assistant != nullptr &&
+         voice_assistant::global_voice_assistant->get_api_connection() == this;
+}
+
 void APIConnection::subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) {
   if (voice_assistant::global_voice_assistant != nullptr) {
     voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe);
   }
 }
 void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) {
-  if (voice_assistant::global_voice_assistant != nullptr) {
-    if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
-      return;
-    }
+  if (!this->check_voice_assistant_api_connection_()) {
+    return;
+  }
 
-    if (msg.error) {
-      voice_assistant::global_voice_assistant->failed_to_start();
-      return;
-    }
-    if (msg.port == 0) {
-      // Use API Audio
-      voice_assistant::global_voice_assistant->start_streaming();
-    } else {
-      struct sockaddr_storage storage;
-      socklen_t len = sizeof(storage);
-      this->helper_->getpeername((struct sockaddr *) &storage, &len);
-      voice_assistant::global_voice_assistant->start_streaming(&storage, msg.port);
-    }
+  if (msg.error) {
+    voice_assistant::global_voice_assistant->failed_to_start();
+    return;
+  }
+  if (msg.port == 0) {
+    // Use API Audio
+    voice_assistant::global_voice_assistant->start_streaming();
+  } else {
+    struct sockaddr_storage storage;
+    socklen_t len = sizeof(storage);
+    this->helper_->getpeername((struct sockaddr *) &storage, &len);
+    voice_assistant::global_voice_assistant->start_streaming(&storage, msg.port);
   }
 };
 void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) {
-  if (voice_assistant::global_voice_assistant != nullptr) {
-    if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
-      return;
-    }
-
+  if (this->check_voice_assistant_api_connection_()) {
     voice_assistant::global_voice_assistant->on_event(msg);
   }
 }
 void APIConnection::on_voice_assistant_audio(const VoiceAssistantAudio &msg) {
-  if (voice_assistant::global_voice_assistant != nullptr) {
-    if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
-      return;
-    }
-
+  if (this->check_voice_assistant_api_connection_()) {
     voice_assistant::global_voice_assistant->on_audio(msg);
   }
 };
 void APIConnection::on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) {
-  if (voice_assistant::global_voice_assistant != nullptr) {
-    if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
-      return;
-    }
-
+  if (this->check_voice_assistant_api_connection_()) {
     voice_assistant::global_voice_assistant->on_timer_event(msg);
   }
 };
 
 void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) {
-  if (voice_assistant::global_voice_assistant != nullptr) {
-    if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
-      return;
-    }
-
+  if (this->check_voice_assistant_api_connection_()) {
     voice_assistant::global_voice_assistant->on_announce(msg);
   }
 }
@@ -1243,35 +1230,29 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno
 VoiceAssistantConfigurationResponse APIConnection::voice_assistant_get_configuration(
     const VoiceAssistantConfigurationRequest &msg) {
   VoiceAssistantConfigurationResponse resp;
-  if (voice_assistant::global_voice_assistant != nullptr) {
-    if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
-      return resp;
-    }
-
-    auto &config = voice_assistant::global_voice_assistant->get_configuration();
-    for (auto &wake_word : config.available_wake_words) {
-      VoiceAssistantWakeWord resp_wake_word;
-      resp_wake_word.id = wake_word.id;
-      resp_wake_word.wake_word = wake_word.wake_word;
-      for (const auto &lang : wake_word.trained_languages) {
-        resp_wake_word.trained_languages.push_back(lang);
-      }
-      resp.available_wake_words.push_back(std::move(resp_wake_word));
-    }
-    for (auto &wake_word_id : config.active_wake_words) {
-      resp.active_wake_words.push_back(wake_word_id);
-    }
-    resp.max_active_wake_words = config.max_active_wake_words;
+  if (!this->check_voice_assistant_api_connection_()) {
+    return resp;
   }
+
+  auto &config = voice_assistant::global_voice_assistant->get_configuration();
+  for (auto &wake_word : config.available_wake_words) {
+    VoiceAssistantWakeWord resp_wake_word;
+    resp_wake_word.id = wake_word.id;
+    resp_wake_word.wake_word = wake_word.wake_word;
+    for (const auto &lang : wake_word.trained_languages) {
+      resp_wake_word.trained_languages.push_back(lang);
+    }
+    resp.available_wake_words.push_back(std::move(resp_wake_word));
+  }
+  for (auto &wake_word_id : config.active_wake_words) {
+    resp.active_wake_words.push_back(wake_word_id);
+  }
+  resp.max_active_wake_words = config.max_active_wake_words;
   return resp;
 }
 
 void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
-  if (voice_assistant::global_voice_assistant != nullptr) {
-    if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
-      return;
-    }
-
+  if (this->check_voice_assistant_api_connection_()) {
     voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words);
   }
 }
diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h
index 166dbc3656..aa323d339d 100644
--- a/esphome/components/api/api_connection.h
+++ b/esphome/components/api/api_connection.h
@@ -301,6 +301,11 @@ class APIConnection : public APIServerConnection {
   static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn,
                                            uint32_t remaining_size, bool is_single);
 
+#ifdef USE_VOICE_ASSISTANT
+  // Helper to check voice assistant validity and connection ownership
+  inline bool check_voice_assistant_api_connection_() const;
+#endif
+
   // Helper method to process multiple entities from an iterator in a batch
   template void process_iterator_batch_(Iterator &iterator) {
     size_t initial_size = this->deferred_batch_.size();

From 42a1f6922fe78b049c00b9494542afe1bf1cb0c5 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 16:16:48 -0500
Subject: [PATCH 273/293] Eliminate bluetooth_proxy guard variable to save 8
 bytes RAM (#9343)

---
 .../components/bluetooth_proxy/bluetooth_proxy.cpp  | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp
index 98f11fac7a..a5e8ec0860 100644
--- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp
+++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp
@@ -58,10 +58,15 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
 // 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload
 // This achieves ~97% WiFi MTU utilization while staying under the limit
 static constexpr size_t FLUSH_BATCH_SIZE = 16;
-static std::vector &get_batch_buffer() {
-  static std::vector batch_buffer;
-  return batch_buffer;
-}
+
+namespace {
+// Batch buffer in anonymous namespace to avoid guard variable (saves 8 bytes)
+// This is initialized at program startup before any threads
+// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
+std::vector batch_buffer;
+}  // namespace
+
+static std::vector &get_batch_buffer() { return batch_buffer; }
 
 bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) {
   if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_)

From c4fac1a2ae134167624aaf33fcac9296f3110b50 Mon Sep 17 00:00:00 2001
From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com>
Date: Mon, 7 Jul 2025 23:21:14 +0200
Subject: [PATCH 274/293] [nextion] Optimize component memory usage with
 bitfield state management (#9373)

---
 .../binary_sensor/nextion_binarysensor.cpp    |  2 +-
 .../components/nextion/nextion_component.cpp  | 56 +++++++--------
 .../components/nextion/nextion_component.h    | 71 ++++++++++++++-----
 .../nextion/sensor/nextion_sensor.cpp         |  2 +-
 .../nextion/switch/nextion_switch.cpp         |  2 +-
 .../text_sensor/nextion_textsensor.cpp        |  2 +-
 6 files changed, 85 insertions(+), 50 deletions(-)

diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp
index b6d4cc3f23..3628ac2f63 100644
--- a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp
+++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp
@@ -44,7 +44,7 @@ void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nexti
     return;
 
   if (send_to_nextion) {
-    if (this->nextion_->is_sleeping() || !this->visible_) {
+    if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
       this->needs_to_send_update_ = true;
     } else {
       this->needs_to_send_update_ = false;
diff --git a/esphome/components/nextion/nextion_component.cpp b/esphome/components/nextion/nextion_component.cpp
index cfb4e3600c..32929d6845 100644
--- a/esphome/components/nextion/nextion_component.cpp
+++ b/esphome/components/nextion/nextion_component.cpp
@@ -8,8 +8,8 @@ void NextionComponent::set_background_color(Color bco) {
     return;  // This is a variable. no need to set color
   }
   this->bco_ = bco;
-  this->bco_needs_update_ = true;
-  this->bco_is_set_ = true;
+  this->component_flags_.bco_needs_update = true;
+  this->component_flags_.bco_is_set = true;
   this->update_component_settings();
 }
 
@@ -19,8 +19,8 @@ void NextionComponent::set_background_pressed_color(Color bco2) {
   }
 
   this->bco2_ = bco2;
-  this->bco2_needs_update_ = true;
-  this->bco2_is_set_ = true;
+  this->component_flags_.bco2_needs_update = true;
+  this->component_flags_.bco2_is_set = true;
   this->update_component_settings();
 }
 
@@ -29,8 +29,8 @@ void NextionComponent::set_foreground_color(Color pco) {
     return;  // This is a variable. no need to set color
   }
   this->pco_ = pco;
-  this->pco_needs_update_ = true;
-  this->pco_is_set_ = true;
+  this->component_flags_.pco_needs_update = true;
+  this->component_flags_.pco_is_set = true;
   this->update_component_settings();
 }
 
@@ -39,8 +39,8 @@ void NextionComponent::set_foreground_pressed_color(Color pco2) {
     return;  // This is a variable. no need to set color
   }
   this->pco2_ = pco2;
-  this->pco2_needs_update_ = true;
-  this->pco2_is_set_ = true;
+  this->component_flags_.pco2_needs_update = true;
+  this->component_flags_.pco2_is_set = true;
   this->update_component_settings();
 }
 
@@ -49,8 +49,8 @@ void NextionComponent::set_font_id(uint8_t font_id) {
     return;  // This is a variable. no need to set color
   }
   this->font_id_ = font_id;
-  this->font_id_needs_update_ = true;
-  this->font_id_is_set_ = true;
+  this->component_flags_.font_id_needs_update = true;
+  this->component_flags_.font_id_is_set = true;
   this->update_component_settings();
 }
 
@@ -58,20 +58,20 @@ void NextionComponent::set_visible(bool visible) {
   if (this->variable_name_ == this->variable_name_to_send_) {
     return;  // This is a variable. no need to set color
   }
-  this->visible_ = visible;
-  this->visible_needs_update_ = true;
-  this->visible_is_set_ = true;
+  this->component_flags_.visible = visible;
+  this->component_flags_.visible_needs_update = true;
+  this->component_flags_.visible_is_set = true;
   this->update_component_settings();
 }
 
 void NextionComponent::update_component_settings(bool force_update) {
-  if (this->nextion_->is_sleeping() || !this->nextion_->is_setup() || !this->visible_is_set_ ||
-      (!this->visible_needs_update_ && !this->visible_)) {
+  if (this->nextion_->is_sleeping() || !this->nextion_->is_setup() || !this->component_flags_.visible_is_set ||
+      (!this->component_flags_.visible_needs_update && !this->component_flags_.visible)) {
     this->needs_to_send_update_ = true;
     return;
   }
 
-  if (this->visible_needs_update_ || (force_update && this->visible_is_set_)) {
+  if (this->component_flags_.visible_needs_update || (force_update && this->component_flags_.visible_is_set)) {
     std::string name_to_send = this->variable_name_;
 
     size_t pos = name_to_send.find_last_of('.');
@@ -79,9 +79,9 @@ void NextionComponent::update_component_settings(bool force_update) {
       name_to_send = name_to_send.substr(pos + 1);
     }
 
-    this->visible_needs_update_ = false;
+    this->component_flags_.visible_needs_update = false;
 
-    if (this->visible_) {
+    if (this->component_flags_.visible) {
       this->nextion_->show_component(name_to_send.c_str());
       this->send_state_to_nextion();
     } else {
@@ -90,26 +90,26 @@ void NextionComponent::update_component_settings(bool force_update) {
     }
   }
 
-  if (this->bco_needs_update_ || (force_update && this->bco2_is_set_)) {
+  if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco2_is_set)) {
     this->nextion_->set_component_background_color(this->variable_name_.c_str(), this->bco_);
-    this->bco_needs_update_ = false;
+    this->component_flags_.bco_needs_update = false;
   }
-  if (this->bco2_needs_update_ || (force_update && this->bco2_is_set_)) {
+  if (this->component_flags_.bco2_needs_update || (force_update && this->component_flags_.bco2_is_set)) {
     this->nextion_->set_component_pressed_background_color(this->variable_name_.c_str(), this->bco2_);
-    this->bco2_needs_update_ = false;
+    this->component_flags_.bco2_needs_update = false;
   }
-  if (this->pco_needs_update_ || (force_update && this->pco_is_set_)) {
+  if (this->component_flags_.pco_needs_update || (force_update && this->component_flags_.pco_is_set)) {
     this->nextion_->set_component_foreground_color(this->variable_name_.c_str(), this->pco_);
-    this->pco_needs_update_ = false;
+    this->component_flags_.pco_needs_update = false;
   }
-  if (this->pco2_needs_update_ || (force_update && this->pco2_is_set_)) {
+  if (this->component_flags_.pco2_needs_update || (force_update && this->component_flags_.pco2_is_set)) {
     this->nextion_->set_component_pressed_foreground_color(this->variable_name_.c_str(), this->pco2_);
-    this->pco2_needs_update_ = false;
+    this->component_flags_.pco2_needs_update = false;
   }
 
-  if (this->font_id_needs_update_ || (force_update && this->font_id_is_set_)) {
+  if (this->component_flags_.font_id_needs_update || (force_update && this->component_flags_.font_id_is_set)) {
     this->nextion_->set_component_font(this->variable_name_.c_str(), this->font_id_);
-    this->font_id_needs_update_ = false;
+    this->component_flags_.font_id_needs_update = false;
   }
 }
 }  // namespace nextion
diff --git a/esphome/components/nextion/nextion_component.h b/esphome/components/nextion/nextion_component.h
index 2f3c4f3c16..add9e11cf1 100644
--- a/esphome/components/nextion/nextion_component.h
+++ b/esphome/components/nextion/nextion_component.h
@@ -21,29 +21,64 @@ class NextionComponent : public NextionComponentBase {
   void set_visible(bool visible);
 
  protected:
+  /**
+   * @brief Constructor initializes component state with visible=true (default state)
+   */
+  NextionComponent() {
+    component_flags_ = {};         // Zero-initialize all state
+    component_flags_.visible = 1;  // Set default visibility to true
+  }
+
   NextionBase *nextion_;
 
-  bool bco_needs_update_ = false;
-  bool bco_is_set_ = false;
-  Color bco_;
-  bool bco2_needs_update_ = false;
-  bool bco2_is_set_ = false;
-  Color bco2_;
-  bool pco_needs_update_ = false;
-  bool pco_is_set_ = false;
-  Color pco_;
-  bool pco2_needs_update_ = false;
-  bool pco2_is_set_ = false;
-  Color pco2_;
+  // Color and styling properties
+  Color bco_;   // Background color
+  Color bco2_;  // Pressed background color
+  Color pco_;   // Foreground color
+  Color pco2_;  // Pressed foreground color
   uint8_t font_id_ = 0;
-  bool font_id_needs_update_ = false;
-  bool font_id_is_set_ = false;
 
-  bool visible_ = true;
-  bool visible_needs_update_ = false;
-  bool visible_is_set_ = false;
+  /**
+   * @brief Component state management using compact bitfield structure
+   *
+   * Stores all component state flags and properties in a single 16-bit bitfield
+   * for efficient memory usage and improved cache locality.
+   *
+   * Each component property maintains two state flags:
+   * - needs_update: Indicates the property requires synchronization with the display
+   * - is_set: Tracks whether the property has been explicitly configured
+   *
+   * The visible field stores both the update flags and the actual visibility state.
+   */
+  struct ComponentState {
+    // Background color flags
+    uint16_t bco_needs_update : 1;
+    uint16_t bco_is_set : 1;
 
-  // void send_state_to_nextion() = 0;
+    // Pressed background color flags
+    uint16_t bco2_needs_update : 1;
+    uint16_t bco2_is_set : 1;
+
+    // Foreground color flags
+    uint16_t pco_needs_update : 1;
+    uint16_t pco_is_set : 1;
+
+    // Pressed foreground color flags
+    uint16_t pco2_needs_update : 1;
+    uint16_t pco2_is_set : 1;
+
+    // Font ID flags
+    uint16_t font_id_needs_update : 1;
+    uint16_t font_id_is_set : 1;
+
+    // Visibility flags
+    uint16_t visible_needs_update : 1;
+    uint16_t visible_is_set : 1;
+    uint16_t visible : 1;  // Actual visibility state
+
+    // Reserved bits for future expansion
+    uint16_t reserved : 3;
+  } component_flags_;
 };
 }  // namespace nextion
 }  // namespace esphome
diff --git a/esphome/components/nextion/sensor/nextion_sensor.cpp b/esphome/components/nextion/sensor/nextion_sensor.cpp
index 0ed9da95d4..03b7261239 100644
--- a/esphome/components/nextion/sensor/nextion_sensor.cpp
+++ b/esphome/components/nextion/sensor/nextion_sensor.cpp
@@ -53,7 +53,7 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) {
 
   if (this->wave_chan_id_ == UINT8_MAX) {
     if (send_to_nextion) {
-      if (this->nextion_->is_sleeping() || !this->visible_) {
+      if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
         this->needs_to_send_update_ = true;
       } else {
         this->needs_to_send_update_ = false;
diff --git a/esphome/components/nextion/switch/nextion_switch.cpp b/esphome/components/nextion/switch/nextion_switch.cpp
index fe71182496..21636f2bfa 100644
--- a/esphome/components/nextion/switch/nextion_switch.cpp
+++ b/esphome/components/nextion/switch/nextion_switch.cpp
@@ -28,7 +28,7 @@ void NextionSwitch::set_state(bool state, bool publish, bool send_to_nextion) {
     return;
 
   if (send_to_nextion) {
-    if (this->nextion_->is_sleeping() || !this->visible_) {
+    if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
       this->needs_to_send_update_ = true;
     } else {
       this->needs_to_send_update_ = false;
diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp
index e08cbb02ca..9b6deeda87 100644
--- a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp
+++ b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp
@@ -26,7 +26,7 @@ void NextionTextSensor::set_state(const std::string &state, bool publish, bool s
     return;
 
   if (send_to_nextion) {
-    if (this->nextion_->is_sleeping() || !this->visible_) {
+    if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
       this->needs_to_send_update_ = true;
     } else {
       this->nextion_->add_no_result_to_queue_with_set(this, state);

From fb357b896581d655246fd73bf8c7ccb958bd28f4 Mon Sep 17 00:00:00 2001
From: Steffen Arntz 
Date: Mon, 7 Jul 2025 23:25:11 +0200
Subject: [PATCH 275/293] Fix brightness setting not working on SSD1305 128x32
 OLEDs (#9376)

---
 esphome/components/ssd1306_base/ssd1306_base.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp
index 0547a77184..1f039cff78 100644
--- a/esphome/components/ssd1306_base/ssd1306_base.cpp
+++ b/esphome/components/ssd1306_base/ssd1306_base.cpp
@@ -224,7 +224,7 @@ bool SSD1306::is_sh1106_() const {
 }
 bool SSD1306::is_sh1107_() const { return this->model_ == SH1107_MODEL_128_64 || this->model_ == SH1107_MODEL_128_128; }
 bool SSD1306::is_ssd1305_() const {
-  return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_64;
+  return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_32;
 }
 void SSD1306::update() {
   this->do_update_();

From ee8ee4e6464c9b1d6a41358b50e0c9ce325287b6 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 17:00:03 -0500
Subject: [PATCH 276/293] Optimize logger callback API by including message
 length parameter (#9368)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
 esphome/components/api/api_connection.cpp    |  7 +++--
 esphome/components/api/api_connection.h      |  2 +-
 esphome/components/api/api_server.cpp        | 25 ++++++++---------
 esphome/components/logger/logger.cpp         | 28 +++++++++++++++++---
 esphome/components/logger/logger.h           |  8 +++---
 esphome/components/mqtt/mqtt_client.cpp      | 17 ++++++------
 esphome/components/syslog/esphome_syslog.cpp |  8 +++---
 esphome/components/syslog/esphome_syslog.h   |  2 +-
 esphome/components/web_server/web_server.cpp |  3 ++-
 9 files changed, 63 insertions(+), 37 deletions(-)

diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp
index 01f4552842..779784e787 100644
--- a/esphome/components/api/api_connection.cpp
+++ b/esphome/components/api/api_connection.cpp
@@ -1392,12 +1392,11 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) {
 }
 #endif
 
-bool APIConnection::try_send_log_message(int level, const char *tag, const char *line) {
+bool APIConnection::try_send_log_message(int level, const char *tag, const char *line, size_t message_len) {
   if (this->flags_.log_subscription < level)
     return false;
 
   // Pre-calculate message size to avoid reallocations
-  const size_t line_length = strlen(line);
   uint32_t msg_size = 0;
 
   // Add size for level field (field ID 1, varint type)
@@ -1406,14 +1405,14 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char
 
   // Add size for string field (field ID 3, string type)
   // 1 byte for field tag + size of length varint + string length
-  msg_size += 1 + api::ProtoSize::varint(static_cast(line_length)) + line_length;
+  msg_size += 1 + api::ProtoSize::varint(static_cast(message_len)) + message_len;
 
   // Create a pre-sized buffer
   auto buffer = this->create_buffer(msg_size);
 
   // Encode the message (SubscribeLogsResponse)
   buffer.encode_uint32(1, static_cast(level));  // LogLevel level = 1
-  buffer.encode_string(3, line, line_length);             // string message = 3
+  buffer.encode_string(3, line, message_len);             // string message = 3
 
   // SubscribeLogsResponse - 29
   return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE);
diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h
index aa323d339d..b70b037999 100644
--- a/esphome/components/api/api_connection.h
+++ b/esphome/components/api/api_connection.h
@@ -107,7 +107,7 @@ class APIConnection : public APIServerConnection {
   bool send_media_player_state(media_player::MediaPlayer *media_player);
   void media_player_command(const MediaPlayerCommandRequest &msg) override;
 #endif
-  bool try_send_log_message(int level, const char *tag, const char *line);
+  bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
   void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
     if (!this->flags_.service_call_subscription)
       return;
diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp
index 70f2ff714d..0915746381 100644
--- a/esphome/components/api/api_server.cpp
+++ b/esphome/components/api/api_server.cpp
@@ -104,18 +104,19 @@ void APIServer::setup() {
 
 #ifdef USE_LOGGER
   if (logger::global_logger != nullptr) {
-    logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) {
-      if (this->shutting_down_) {
-        // Don't try to send logs during shutdown
-        // as it could result in a recursion and
-        // we would be filling a buffer we are trying to clear
-        return;
-      }
-      for (auto &c : this->clients_) {
-        if (!c->flags_.remove)
-          c->try_send_log_message(level, tag, message);
-      }
-    });
+    logger::global_logger->add_on_log_callback(
+        [this](int level, const char *tag, const char *message, size_t message_len) {
+          if (this->shutting_down_) {
+            // Don't try to send logs during shutdown
+            // as it could result in a recursion and
+            // we would be filling a buffer we are trying to clear
+            return;
+          }
+          for (auto &c : this->clients_) {
+            if (!c->flags_.remove)
+              c->try_send_log_message(level, tag, message, message_len);
+          }
+        });
   }
 #endif
 
diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp
index a2c2aa0320..db807f7e53 100644
--- a/esphome/components/logger/logger.cpp
+++ b/esphome/components/logger/logger.cpp
@@ -90,6 +90,25 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
 #ifdef USE_STORE_LOG_STR_IN_FLASH
 // Implementation for ESP8266 with flash string support.
 // Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266.
+//
+// This function handles format strings stored in flash memory (PROGMEM) to save RAM.
+// The buffer is used in a special way to avoid allocating extra memory:
+//
+// Memory layout during execution:
+// Step 1: Copy format string from flash to buffer
+//         tx_buffer_: [format_string][null][.....................]
+//         tx_buffer_at_: ------------------^
+//         msg_start: saved here -----------^
+//
+// Step 2: format_log_to_buffer_with_terminator_ reads format string from beginning
+//         and writes formatted output starting at msg_start position
+//         tx_buffer_: [format_string][null][formatted_message][null]
+//         tx_buffer_at_: -------------------------------------^
+//
+// Step 3: Output the formatted message (starting at msg_start)
+//         write_msg_ and callbacks receive: this->tx_buffer_ + msg_start
+//         which points to: [formatted_message][null]
+//
 void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format,
                           va_list args) {  // NOLINT
   if (level > this->level_for(tag) || global_recursion_guard_)
@@ -121,7 +140,9 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
   if (this->baud_rate_ > 0) {
     this->write_msg_(this->tx_buffer_ + msg_start);
   }
-  this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start);
+  size_t msg_length =
+      this->tx_buffer_at_ - msg_start;  // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position
+  this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length);
 
   global_recursion_guard_ = false;
 }
@@ -185,7 +206,8 @@ void Logger::loop() {
                                   this->tx_buffer_size_);
       this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_);
       this->tx_buffer_[this->tx_buffer_at_] = '\0';
-      this->log_callback_.call(message->level, message->tag, this->tx_buffer_);
+      size_t msg_len = this->tx_buffer_at_;  // We already know the length from tx_buffer_at_
+      this->log_callback_.call(message->level, message->tag, this->tx_buffer_, msg_len);
       // At this point all the data we need from message has been transferred to the tx_buffer
       // so we can release the message to allow other tasks to use it as soon as possible.
       this->log_buffer_->release_message_main_loop(received_token);
@@ -214,7 +236,7 @@ void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->lo
 UARTSelection Logger::get_uart() const { return this->uart_; }
 #endif
 
-void Logger::add_on_log_callback(std::function &&callback) {
+void Logger::add_on_log_callback(std::function &&callback) {
   this->log_callback_.add(std::move(callback));
 }
 float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; }
diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h
index e376d9fbf5..fb68e75a51 100644
--- a/esphome/components/logger/logger.h
+++ b/esphome/components/logger/logger.h
@@ -143,7 +143,7 @@ class Logger : public Component {
   inline uint8_t level_for(const char *tag);
 
   /// Register a callback that will be called for every log message sent
-  void add_on_log_callback(std::function &&callback);
+  void add_on_log_callback(std::function &&callback);
 
   // add a listener for log level changes
   void add_listener(std::function &&callback) { this->level_callback_.add(std::move(callback)); }
@@ -192,7 +192,7 @@ class Logger : public Component {
     if (this->baud_rate_ > 0) {
       this->write_msg_(this->tx_buffer_);  // If logging is enabled, write to console
     }
-    this->log_callback_.call(level, tag, this->tx_buffer_);
+    this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_);
   }
 
   // Write the body of the log message to the buffer
@@ -246,7 +246,7 @@ class Logger : public Component {
 
   // Large objects (internally aligned)
   std::map log_levels_{};
-  CallbackManager log_callback_{};
+  CallbackManager log_callback_{};
   CallbackManager level_callback_{};
 #ifdef USE_ESPHOME_TASK_LOG_BUFFER
   std::unique_ptr log_buffer_;  // Will be initialized with init_log_buffer
@@ -385,7 +385,7 @@ class LoggerMessageTrigger : public Trigger
  public:
   explicit LoggerMessageTrigger(Logger *parent, uint8_t level) {
     this->level_ = level;
-    parent->add_on_log_callback([this](uint8_t level, const char *tag, const char *message) {
+    parent->add_on_log_callback([this](uint8_t level, const char *tag, const char *message, size_t message_len) {
       if (level <= this->level_) {
         this->trigger(level, tag, message);
       }
diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp
index 20e0b4a499..ab7fd15a35 100644
--- a/esphome/components/mqtt/mqtt_client.cpp
+++ b/esphome/components/mqtt/mqtt_client.cpp
@@ -57,14 +57,15 @@ void MQTTClientComponent::setup() {
   });
 #ifdef USE_LOGGER
   if (this->is_log_message_enabled() && logger::global_logger != nullptr) {
-    logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) {
-      if (level <= this->log_level_ && this->is_connected()) {
-        this->publish({.topic = this->log_message_.topic,
-                       .payload = message,
-                       .qos = this->log_message_.qos,
-                       .retain = this->log_message_.retain});
-      }
-    });
+    logger::global_logger->add_on_log_callback(
+        [this](int level, const char *tag, const char *message, size_t message_len) {
+          if (level <= this->log_level_ && this->is_connected()) {
+            this->publish({.topic = this->log_message_.topic,
+                           .payload = std::string(message, message_len),
+                           .qos = this->log_message_.qos,
+                           .retain = this->log_message_.retain});
+          }
+        });
   }
 #endif
 
diff --git a/esphome/components/syslog/esphome_syslog.cpp b/esphome/components/syslog/esphome_syslog.cpp
index 9d2cda549b..e322a6951d 100644
--- a/esphome/components/syslog/esphome_syslog.cpp
+++ b/esphome/components/syslog/esphome_syslog.cpp
@@ -21,10 +21,12 @@ constexpr int LOG_LEVEL_TO_SYSLOG_SEVERITY[] = {
 
 void Syslog::setup() {
   logger::global_logger->add_on_log_callback(
-      [this](int level, const char *tag, const char *message) { this->log_(level, tag, message); });
+      [this](int level, const char *tag, const char *message, size_t message_len) {
+        this->log_(level, tag, message, message_len);
+      });
 }
 
-void Syslog::log_(const int level, const char *tag, const char *message) const {
+void Syslog::log_(const int level, const char *tag, const char *message, size_t message_len) const {
   if (level > this->log_level_)
     return;
   // Syslog PRI calculation: facility * 8 + severity
@@ -34,7 +36,7 @@ void Syslog::log_(const int level, const char *tag, const char *message) const {
   }
   int pri = this->facility_ * 8 + severity;
   auto timestamp = this->time_->now().strftime("%b %d %H:%M:%S");
-  unsigned len = strlen(message);
+  size_t len = message_len;
   // remove color formatting
   if (this->strip_ && message[0] == 0x1B && len > 11) {
     message += 7;
diff --git a/esphome/components/syslog/esphome_syslog.h b/esphome/components/syslog/esphome_syslog.h
index 421a9bee73..e3b2f7dae5 100644
--- a/esphome/components/syslog/esphome_syslog.h
+++ b/esphome/components/syslog/esphome_syslog.h
@@ -17,7 +17,7 @@ class Syslog : public Component, public Parented {
 
  protected:
   int log_level_;
-  void log_(int level, const char *tag, const char *message) const;
+  void log_(int level, const char *tag, const char *message, size_t message_len) const;
   time::RealTimeClock *time_;
   bool strip_{true};
   int facility_{16};
diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp
index 20ff1a7c29..8ced5b7e18 100644
--- a/esphome/components/web_server/web_server.cpp
+++ b/esphome/components/web_server/web_server.cpp
@@ -287,7 +287,8 @@ void WebServer::setup() {
   if (logger::global_logger != nullptr && this->expose_log_) {
     logger::global_logger->add_on_log_callback(
         // logs are not deferred, the memory overhead would be too large
-        [this](int level, const char *tag, const char *message) {
+        [this](int level, const char *tag, const char *message, size_t message_len) {
+          (void) message_len;
           this->events_.try_send_nodefer(message, "log", millis());
         });
   }

From 7150f2806f337caf101b402b31076bf7650bea43 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 17:14:34 -0500
Subject: [PATCH 277/293] Run integration tests only on Python 3.13 to reduce
 CI resource usage (#9377)

---
 .github/workflows/ci.yml | 39 +++++++++++++++++++++++++++++++++++++--
 1 file changed, 37 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ca6d1b0aac..b7546012e9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -214,17 +214,51 @@ jobs:
         if: matrix.os == 'windows-latest'
         run: |
           ./venv/Scripts/activate
-          pytest -vv --cov-report=xml --tb=native -n auto tests
+          pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
       - name: Run pytest
         if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
         run: |
           . venv/bin/activate
-          pytest -vv --cov-report=xml --tb=native -n auto tests
+          pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
       - name: Upload coverage to Codecov
         uses: codecov/codecov-action@v5.4.3
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
 
+  integration-tests:
+    name: Run integration tests
+    runs-on: ubuntu-latest
+    needs:
+      - common
+    steps:
+      - name: Check out code from GitHub
+        uses: actions/checkout@v4.2.2
+      - name: Set up Python 3.13
+        id: python
+        uses: actions/setup-python@v5.6.0
+        with:
+          python-version: "3.13"
+      - name: Restore Python virtual environment
+        id: cache-venv
+        uses: actions/cache@v4.2.3
+        with:
+          path: venv
+          key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
+      - name: Create Python virtual environment
+        if: steps.cache-venv.outputs.cache-hit != 'true'
+        run: |
+          python -m venv venv
+          . venv/bin/activate
+          python --version
+          pip install -r requirements.txt -r requirements_test.txt
+          pip install -e .
+      - name: Register matcher
+        run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
+      - name: Run integration tests
+        run: |
+          . venv/bin/activate
+          pytest -vv --no-cov --tb=native -n auto tests/integration/
+
   clang-format:
     name: Check clang-format
     runs-on: ubuntu-24.04
@@ -494,6 +528,7 @@ jobs:
       - flake8
       - pylint
       - pytest
+      - integration-tests
       - pyupgrade
       - clang-tidy
       - list-components

From a72905191a2c8e97573840757fa7699d6d7df9e3 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 18:08:21 -0500
Subject: [PATCH 278/293] Fix flaky test_api_conditional_memory and improve
 integration test patterns (#9379)

---
 tests/integration/README.md                   | 265 ++++++++++++++++++
 .../fixtures/api_conditional_memory.yaml      |  43 ---
 .../test_api_conditional_memory.py            | 179 +++---------
 tests/integration/test_api_vv_logging.py      |   4 +-
 tests/integration/test_areas_and_devices.py   |   4 +-
 tests/integration/test_device_id_in_state.py  |  54 ++--
 tests/integration/test_entity_icon.py         |   6 -
 .../test_host_mode_entity_fields.py           |   4 +-
 .../test_host_mode_many_entities.py           |   8 +-
 tests/integration/test_host_mode_sensor.py    |   5 +-
 tests/integration/test_light_calls.py         |   3 +-
 11 files changed, 341 insertions(+), 234 deletions(-)

diff --git a/tests/integration/README.md b/tests/integration/README.md
index 26bd5a00ee..8fce81bb80 100644
--- a/tests/integration/README.md
+++ b/tests/integration/README.md
@@ -78,3 +78,268 @@ pytest -s tests/integration/test_host_mode_basic.py
 - Each test gets its own temporary directory and unique port
 - Port allocation minimizes race conditions by holding the socket until just before ESPHome starts
 - Output from ESPHome processes is displayed for debugging
+
+## Integration Test Writing Guide
+
+### Test Patterns and Best Practices
+
+#### 1. Test File Naming Convention
+- Use descriptive names: `test_{category}_{feature}.py`
+- Common categories: `host_mode`, `api`, `scheduler`, `light`, `areas_and_devices`
+- Examples:
+  - `test_host_mode_basic.py` - Basic host mode functionality
+  - `test_api_message_batching.py` - API message batching
+  - `test_scheduler_stress.py` - Scheduler stress testing
+
+#### 2. Essential Imports
+```python
+from __future__ import annotations
+
+import asyncio
+from typing import Any
+
+import pytest
+from aioesphomeapi import EntityState, SensorState
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+```
+
+#### 3. Common Test Patterns
+
+##### Basic Entity Test
+```python
+@pytest.mark.asyncio
+async def test_my_sensor(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test sensor functionality."""
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Get entity list
+        entities, services = await client.list_entities_services()
+
+        # Find specific entity
+        sensor = next((e for e in entities if e.object_id == "my_sensor"), None)
+        assert sensor is not None
+```
+
+##### State Subscription Pattern
+```python
+# Track state changes with futures
+loop = asyncio.get_running_loop()
+states: dict[int, EntityState] = {}
+state_future: asyncio.Future[EntityState] = loop.create_future()
+
+def on_state(state: EntityState) -> None:
+    states[state.key] = state
+    # Check for specific condition using isinstance
+    if isinstance(state, SensorState) and state.state == expected_value:
+        if not state_future.done():
+            state_future.set_result(state)
+
+client.subscribe_states(on_state)
+
+# Wait for state with timeout
+try:
+    result = await asyncio.wait_for(state_future, timeout=5.0)
+except asyncio.TimeoutError:
+    pytest.fail(f"Expected state not received. Got: {list(states.values())}")
+```
+
+##### Service Execution Pattern
+```python
+# Find and execute service
+entities, services = await client.list_entities_services()
+my_service = next((s for s in services if s.name == "my_service"), None)
+assert my_service is not None
+
+# Execute with parameters
+client.execute_service(my_service, {"param1": "value1", "param2": 42})
+```
+
+##### Multiple Entity Tracking
+```python
+# For tests with many entities
+loop = asyncio.get_running_loop()
+entity_count = 50
+received_states: set[int] = set()
+all_states_future: asyncio.Future[bool] = loop.create_future()
+
+def on_state(state: EntityState) -> None:
+    received_states.add(state.key)
+    if len(received_states) >= entity_count and not all_states_future.done():
+        all_states_future.set_result(True)
+
+client.subscribe_states(on_state)
+await asyncio.wait_for(all_states_future, timeout=10.0)
+```
+
+#### 4. YAML Fixture Guidelines
+
+##### Naming Convention
+- Match test function name: `test_my_feature` → `fixtures/my_feature.yaml`
+- Note: Remove `test_` prefix for fixture filename
+
+##### Basic Structure
+```yaml
+esphome:
+  name: test-name  # Use kebab-case
+  # Optional: areas, devices, platformio_options
+
+host:  # Always use host platform for integration tests
+api:   # Port injected automatically
+logger:
+  level: DEBUG  # Optional: Set log level
+
+# Component configurations
+sensor:
+  - platform: template
+    name: "My Sensor"
+    id: my_sensor
+    lambda: return 42.0;
+    update_interval: 0.1s  # Fast updates for testing
+```
+
+##### Advanced Features
+```yaml
+# External components for custom test code
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH  # Replaced by test framework
+    components: [my_test_component]
+
+# Areas and devices
+esphome:
+  name: test-device
+  areas:
+    - id: living_room
+      name: "Living Room"
+    - id: kitchen
+      name: "Kitchen"
+      parent_id: living_room
+  devices:
+    - id: my_device
+      name: "Test Device"
+      area_id: living_room
+
+# API services
+api:
+  services:
+    - service: test_service
+      variables:
+        my_param: string
+      then:
+        - logger.log:
+            format: "Service called with: %s"
+            args: [my_param.c_str()]
+```
+
+#### 5. Testing Complex Scenarios
+
+##### External Components
+Create C++ components in `fixtures/external_components/` for:
+- Stress testing
+- Custom entity behaviors
+- Scheduler testing
+- Memory management tests
+
+##### Log Line Monitoring
+```python
+log_lines: list[str] = []
+
+def on_log_line(line: str) -> None:
+    log_lines.append(line)
+    if "expected message" in line:
+        # Handle specific log messages
+
+async with run_compiled(yaml_config, line_callback=on_log_line):
+    # Test implementation
+```
+
+Example using futures for specific log patterns:
+```python
+import re
+
+loop = asyncio.get_running_loop()
+connected_future = loop.create_future()
+service_future = loop.create_future()
+
+# Patterns to match
+connected_pattern = re.compile(r"Client .* connected from")
+service_pattern = re.compile(r"Service called")
+
+def check_output(line: str) -> None:
+    """Check log output for expected messages."""
+    if not connected_future.done() and connected_pattern.search(line):
+        connected_future.set_result(True)
+    elif not service_future.done() and service_pattern.search(line):
+        service_future.set_result(True)
+
+async with run_compiled(yaml_config, line_callback=check_output):
+    async with api_client_connected() as client:
+        # Wait for specific log message
+        await asyncio.wait_for(connected_future, timeout=5.0)
+
+        # Do test actions...
+
+        # Wait for service log
+        await asyncio.wait_for(service_future, timeout=5.0)
+```
+
+**Note**: Tests that monitor log messages typically have fewer race conditions compared to state-based testing, making them more reliable. However, be aware that the host platform currently does not have a thread-safe logger, so logging from threads will not work correctly.
+
+##### Timeout Handling
+```python
+# Always use timeouts for async operations
+try:
+    result = await asyncio.wait_for(some_future, timeout=5.0)
+except asyncio.TimeoutError:
+    pytest.fail("Operation timed out - check test expectations")
+```
+
+#### 6. Common Assertions
+
+```python
+# Device info
+assert device_info.name == "expected-name"
+assert device_info.compilation_time is not None
+
+# Entity properties
+assert sensor.accuracy_decimals == 2
+assert sensor.state_class == 1  # measurement
+assert sensor.force_update is True
+
+# Service availability
+assert len(services) > 0
+assert any(s.name == "expected_service" for s in services)
+
+# State values
+assert state.state == expected_value
+assert state.missing_state is False
+```
+
+#### 7. Debugging Tips
+
+- Use `pytest -s` to see ESPHome output during tests
+- Add descriptive failure messages to assertions
+- Use `pytest.fail()` with detailed error info for timeouts
+- Check `log_lines` for compilation or runtime errors
+- Enable debug logging in YAML fixtures when needed
+
+#### 8. Performance Considerations
+
+- Use short update intervals (0.1s) for faster tests
+- Set reasonable timeouts (5-10s for most operations)
+- Batch multiple assertions when possible
+- Clean up resources properly using context managers
+
+#### 9. Test Categories
+
+- **Basic Tests**: Minimal functionality verification
+- **Entity Tests**: Sensor, switch, light behavior
+- **API Tests**: Message batching, services, events
+- **Scheduler Tests**: Timing, defer operations, stress
+- **Memory Tests**: Conditional compilation, optimization
+- **Integration Tests**: Areas, devices, complex interactions
diff --git a/tests/integration/fixtures/api_conditional_memory.yaml b/tests/integration/fixtures/api_conditional_memory.yaml
index 49412c3bfe..22e8ed79d6 100644
--- a/tests/integration/fixtures/api_conditional_memory.yaml
+++ b/tests/integration/fixtures/api_conditional_memory.yaml
@@ -2,14 +2,10 @@ esphome:
   name: api-conditional-memory-test
 host:
 api:
-  batch_delay: 0ms
   actions:
     - action: test_simple_service
       then:
         - logger.log: "Simple service called"
-        - binary_sensor.template.publish:
-            id: service_called_sensor
-            state: ON
     - action: test_service_with_args
       variables:
         arg_string: string
@@ -20,53 +16,14 @@ api:
         - logger.log:
             format: "Service called with: %s, %d, %d, %.2f"
             args: [arg_string.c_str(), arg_int, arg_bool, arg_float]
-        - sensor.template.publish:
-            id: service_arg_sensor
-            state: !lambda 'return arg_float;'
   on_client_connected:
     - logger.log:
         format: "Client %s connected from %s"
         args: [client_info.c_str(), client_address.c_str()]
-    - binary_sensor.template.publish:
-        id: client_connected
-        state: ON
-    - text_sensor.template.publish:
-        id: last_client_info
-        state: !lambda 'return client_info;'
   on_client_disconnected:
     - logger.log:
         format: "Client %s disconnected from %s"
         args: [client_info.c_str(), client_address.c_str()]
-    - binary_sensor.template.publish:
-        id: client_connected
-        state: OFF
-    - binary_sensor.template.publish:
-        id: client_disconnected_event
-        state: ON
 
 logger:
   level: DEBUG
-
-binary_sensor:
-  - platform: template
-    name: "Client Connected"
-    id: client_connected
-    device_class: connectivity
-  - platform: template
-    name: "Client Disconnected Event"
-    id: client_disconnected_event
-  - platform: template
-    name: "Service Called"
-    id: service_called_sensor
-
-sensor:
-  - platform: template
-    name: "Service Argument Value"
-    id: service_arg_sensor
-    unit_of_measurement: ""
-    accuracy_decimals: 2
-
-text_sensor:
-  - platform: template
-    name: "Last Client Info"
-    id: last_client_info
diff --git a/tests/integration/test_api_conditional_memory.py b/tests/integration/test_api_conditional_memory.py
index 8048624f70..cfa32c431d 100644
--- a/tests/integration/test_api_conditional_memory.py
+++ b/tests/integration/test_api_conditional_memory.py
@@ -3,15 +3,9 @@
 from __future__ import annotations
 
 import asyncio
+import re
 
-from aioesphomeapi import (
-    BinarySensorInfo,
-    EntityState,
-    SensorInfo,
-    TextSensorInfo,
-    UserService,
-    UserServiceArgType,
-)
+from aioesphomeapi import UserService, UserServiceArgType
 import pytest
 
 from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -25,50 +19,45 @@ async def test_api_conditional_memory(
 ) -> None:
     """Test API triggers and services work correctly with conditional compilation."""
     loop = asyncio.get_running_loop()
-    # Keep ESPHome process running throughout the test
-    async with run_compiled(yaml_config):
-        # First connection
+
+    # Track log messages
+    connected_future = loop.create_future()
+    disconnected_future = loop.create_future()
+    service_simple_future = loop.create_future()
+    service_args_future = loop.create_future()
+
+    # Patterns to match in logs
+    connected_pattern = re.compile(r"Client .* connected from")
+    disconnected_pattern = re.compile(r"Client .* disconnected from")
+    service_simple_pattern = re.compile(r"Simple service called")
+    service_args_pattern = re.compile(
+        r"Service called with: test_string, 123, 1, 42\.50"
+    )
+
+    def check_output(line: str) -> None:
+        """Check log output for expected messages."""
+        if not connected_future.done() and connected_pattern.search(line):
+            connected_future.set_result(True)
+        elif not disconnected_future.done() and disconnected_pattern.search(line):
+            disconnected_future.set_result(True)
+        elif not service_simple_future.done() and service_simple_pattern.search(line):
+            service_simple_future.set_result(True)
+        elif not service_args_future.done() and service_args_pattern.search(line):
+            service_args_future.set_result(True)
+
+    # Run with log monitoring
+    async with run_compiled(yaml_config, line_callback=check_output):
         async with api_client_connected() as client:
             # Verify device info
             device_info = await client.device_info()
             assert device_info is not None
             assert device_info.name == "api-conditional-memory-test"
 
-            # List entities and services
-            entity_info, services = await asyncio.wait_for(
-                client.list_entities_services(), timeout=5.0
-            )
+            # Wait for connection log
+            await asyncio.wait_for(connected_future, timeout=5.0)
 
-            # Find our entities
-            client_connected: BinarySensorInfo | None = None
-            client_disconnected_event: BinarySensorInfo | None = None
-            service_called_sensor: BinarySensorInfo | None = None
-            service_arg_sensor: SensorInfo | None = None
-            last_client_info: TextSensorInfo | None = None
-
-            for entity in entity_info:
-                if isinstance(entity, BinarySensorInfo):
-                    if entity.object_id == "client_connected":
-                        client_connected = entity
-                    elif entity.object_id == "client_disconnected_event":
-                        client_disconnected_event = entity
-                    elif entity.object_id == "service_called":
-                        service_called_sensor = entity
-                elif isinstance(entity, SensorInfo):
-                    if entity.object_id == "service_argument_value":
-                        service_arg_sensor = entity
-                elif isinstance(entity, TextSensorInfo):
-                    if entity.object_id == "last_client_info":
-                        last_client_info = entity
-
-            # Verify all entities exist
-            assert client_connected is not None, "client_connected sensor not found"
-            assert client_disconnected_event is not None, (
-                "client_disconnected_event sensor not found"
-            )
-            assert service_called_sensor is not None, "service_called sensor not found"
-            assert service_arg_sensor is not None, "service_arg_sensor not found"
-            assert last_client_info is not None, "last_client_info sensor not found"
+            # List services
+            _, services = await client.list_entities_services()
 
             # Verify services exist
             assert len(services) == 2, f"Expected 2 services, found {len(services)}"
@@ -98,66 +87,11 @@ async def test_api_conditional_memory(
             assert arg_types["arg_bool"] == UserServiceArgType.BOOL
             assert arg_types["arg_float"] == UserServiceArgType.FLOAT
 
-            # Track state changes
-            states: dict[int, EntityState] = {}
-            states_future: asyncio.Future[None] = loop.create_future()
-
-            def on_state(state: EntityState) -> None:
-                states[state.key] = state
-                # Check if we have initial states for connection sensors
-                if (
-                    client_connected.key in states
-                    and last_client_info.key in states
-                    and not states_future.done()
-                ):
-                    states_future.set_result(None)
-
-            client.subscribe_states(on_state)
-
-            # Wait for initial states
-            await asyncio.wait_for(states_future, timeout=5.0)
-
-            # Verify on_client_connected trigger fired
-            connected_state = states.get(client_connected.key)
-            assert connected_state is not None
-            assert connected_state.state is True, "Client should be connected"
-
-            # Verify client info was captured
-            client_info_state = states.get(last_client_info.key)
-            assert client_info_state is not None
-            assert isinstance(client_info_state.state, str)
-            assert len(client_info_state.state) > 0, "Client info should not be empty"
-
-            # Test simple service
-            service_future: asyncio.Future[None] = loop.create_future()
-
-            def check_service_called(state: EntityState) -> None:
-                if state.key == service_called_sensor.key and state.state is True:
-                    if not service_future.done():
-                        service_future.set_result(None)
-
-            # Update callback to check for service execution
-            client.subscribe_states(check_service_called)
-
             # Call simple service
             client.execute_service(simple_service, {})
 
-            # Wait for service to execute
-            await asyncio.wait_for(service_future, timeout=5.0)
-
-            # Test service with arguments
-            arg_future: asyncio.Future[None] = loop.create_future()
-            expected_float = 42.5
-
-            def check_arg_sensor(state: EntityState) -> None:
-                if (
-                    state.key == service_arg_sensor.key
-                    and abs(state.state - expected_float) < 0.01
-                ):
-                    if not arg_future.done():
-                        arg_future.set_result(None)
-
-            client.subscribe_states(check_arg_sensor)
+            # Wait for service log
+            await asyncio.wait_for(service_simple_future, timeout=5.0)
 
             # Call service with arguments
             client.execute_service(
@@ -166,43 +100,12 @@ async def test_api_conditional_memory(
                     "arg_string": "test_string",
                     "arg_int": 123,
                     "arg_bool": True,
-                    "arg_float": expected_float,
+                    "arg_float": 42.5,
                 },
             )
 
-            # Wait for service with args to execute
-            await asyncio.wait_for(arg_future, timeout=5.0)
+            # Wait for service with args log
+            await asyncio.wait_for(service_args_future, timeout=5.0)
 
-        # After disconnecting first client, reconnect and verify triggers work
-        async with api_client_connected() as client2:
-            # Subscribe to states with new client
-            states2: dict[int, EntityState] = {}
-            states_ready_future: asyncio.Future[None] = loop.create_future()
-
-            def on_state2(state: EntityState) -> None:
-                states2[state.key] = state
-                # Check if we have received both required states
-                if (
-                    client_connected.key in states2
-                    and client_disconnected_event.key in states2
-                    and not states_ready_future.done()
-                ):
-                    states_ready_future.set_result(None)
-
-            client2.subscribe_states(on_state2)
-
-            # Wait for both connected and disconnected event states
-            await asyncio.wait_for(states_ready_future, timeout=5.0)
-
-            # Verify client is connected again (on_client_connected fired)
-            assert states2[client_connected.key].state is True, (
-                "Client should be reconnected"
-            )
-
-            # The client_disconnected_event should be ON from when we disconnected
-            # (it was set ON by on_client_disconnected trigger)
-            disconnected_state = states2.get(client_disconnected_event.key)
-            assert disconnected_state is not None
-            assert disconnected_state.state is True, (
-                "Disconnect event should be ON from previous disconnect"
-            )
+        # Client disconnected here, wait for disconnect log
+        await asyncio.wait_for(disconnected_future, timeout=5.0)
diff --git a/tests/integration/test_api_vv_logging.py b/tests/integration/test_api_vv_logging.py
index 19aab2001c..fcbdd341ae 100644
--- a/tests/integration/test_api_vv_logging.py
+++ b/tests/integration/test_api_vv_logging.py
@@ -5,7 +5,7 @@ from __future__ import annotations
 import asyncio
 from typing import Any
 
-from aioesphomeapi import LogLevel
+from aioesphomeapi import LogLevel, SensorInfo
 import pytest
 
 from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -63,7 +63,7 @@ async def test_api_vv_logging(
         entity_info, _ = await client.list_entities_services()
 
         # Count sensors
-        sensor_count = sum(1 for e in entity_info if hasattr(e, "unit_of_measurement"))
+        sensor_count = sum(1 for e in entity_info if isinstance(e, SensorInfo))
         assert sensor_count >= 10, f"Expected at least 10 sensors, got {sensor_count}"
 
         # Wait for sensor updates to flow with VV logging active
diff --git a/tests/integration/test_areas_and_devices.py b/tests/integration/test_areas_and_devices.py
index 4ce55a30a7..4184255724 100644
--- a/tests/integration/test_areas_and_devices.py
+++ b/tests/integration/test_areas_and_devices.py
@@ -76,8 +76,8 @@ async def test_areas_and_devices(
         # Get entity list to verify device_id mapping
         entities = await client.list_entities_services()
 
-        # Collect sensor entities
-        sensor_entities = [e for e in entities[0] if hasattr(e, "device_id")]
+        # Collect sensor entities (all entities have device_id)
+        sensor_entities = entities[0]
         assert len(sensor_entities) >= 4, (
             f"Expected at least 4 sensor entities, got {len(sensor_entities)}"
         )
diff --git a/tests/integration/test_device_id_in_state.py b/tests/integration/test_device_id_in_state.py
index 3c5181595f..eaa91ec92e 100644
--- a/tests/integration/test_device_id_in_state.py
+++ b/tests/integration/test_device_id_in_state.py
@@ -4,7 +4,7 @@ from __future__ import annotations
 
 import asyncio
 
-from aioesphomeapi import EntityState
+from aioesphomeapi import BinarySensorState, EntityState, SensorState, TextSensorState
 import pytest
 
 from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -40,28 +40,22 @@ async def test_device_id_in_state(
         entity_device_mapping: dict[int, int] = {}
 
         for entity in all_entities:
-            if hasattr(entity, "name") and hasattr(entity, "key"):
-                if entity.name == "Temperature":
-                    entity_device_mapping[entity.key] = device_ids[
-                        "Temperature Monitor"
-                    ]
-                elif entity.name == "Humidity":
-                    entity_device_mapping[entity.key] = device_ids["Humidity Monitor"]
-                elif entity.name == "Motion Detected":
-                    entity_device_mapping[entity.key] = device_ids["Motion Sensor"]
-                elif entity.name == "Temperature Monitor Power":
-                    entity_device_mapping[entity.key] = device_ids[
-                        "Temperature Monitor"
-                    ]
-                elif entity.name == "Temperature Status":
-                    entity_device_mapping[entity.key] = device_ids[
-                        "Temperature Monitor"
-                    ]
-                elif entity.name == "Motion Light":
-                    entity_device_mapping[entity.key] = device_ids["Motion Sensor"]
-                elif entity.name == "No Device Sensor":
-                    # Entity without device_id should have device_id 0
-                    entity_device_mapping[entity.key] = 0
+            # All entities have name and key attributes
+            if entity.name == "Temperature":
+                entity_device_mapping[entity.key] = device_ids["Temperature Monitor"]
+            elif entity.name == "Humidity":
+                entity_device_mapping[entity.key] = device_ids["Humidity Monitor"]
+            elif entity.name == "Motion Detected":
+                entity_device_mapping[entity.key] = device_ids["Motion Sensor"]
+            elif entity.name == "Temperature Monitor Power":
+                entity_device_mapping[entity.key] = device_ids["Temperature Monitor"]
+            elif entity.name == "Temperature Status":
+                entity_device_mapping[entity.key] = device_ids["Temperature Monitor"]
+            elif entity.name == "Motion Light":
+                entity_device_mapping[entity.key] = device_ids["Motion Sensor"]
+            elif entity.name == "No Device Sensor":
+                # Entity without device_id should have device_id 0
+                entity_device_mapping[entity.key] = 0
 
         assert len(entity_device_mapping) >= 6, (
             f"Expected at least 6 mapped entities, got {len(entity_device_mapping)}"
@@ -111,7 +105,7 @@ async def test_device_id_in_state(
             (
                 s
                 for s in states.values()
-                if hasattr(s, "state")
+                if isinstance(s, SensorState)
                 and isinstance(s.state, float)
                 and s.device_id != 0
             ),
@@ -122,11 +116,7 @@ async def test_device_id_in_state(
 
         # Find a binary sensor state
         binary_sensor_state = next(
-            (
-                s
-                for s in states.values()
-                if hasattr(s, "state") and isinstance(s.state, bool)
-            ),
+            (s for s in states.values() if isinstance(s, BinarySensorState)),
             None,
         )
         assert binary_sensor_state is not None, "No binary sensor state found"
@@ -136,11 +126,7 @@ async def test_device_id_in_state(
 
         # Find a text sensor state
         text_sensor_state = next(
-            (
-                s
-                for s in states.values()
-                if hasattr(s, "state") and isinstance(s.state, str)
-            ),
+            (s for s in states.values() if isinstance(s, TextSensorState)),
             None,
         )
         assert text_sensor_state is not None, "No text sensor state found"
diff --git a/tests/integration/test_entity_icon.py b/tests/integration/test_entity_icon.py
index 56e266b486..aec7168165 100644
--- a/tests/integration/test_entity_icon.py
+++ b/tests/integration/test_entity_icon.py
@@ -51,9 +51,6 @@ async def test_entity_icon(
             entity = entity_map[entity_name]
 
             # Check icon field
-            assert hasattr(entity, "icon"), (
-                f"{entity_name}: Entity should have icon attribute"
-            )
             assert entity.icon == expected_icon, (
                 f"{entity_name}: icon mismatch - "
                 f"expected '{expected_icon}', got '{entity.icon}'"
@@ -67,9 +64,6 @@ async def test_entity_icon(
             entity = entity_map[entity_name]
 
             # Check icon field is empty
-            assert hasattr(entity, "icon"), (
-                f"{entity_name}: Entity should have icon attribute"
-            )
             assert entity.icon == "", (
                 f"{entity_name}: icon should be empty string for entities without icons, "
                 f"got '{entity.icon}'"
diff --git a/tests/integration/test_host_mode_entity_fields.py b/tests/integration/test_host_mode_entity_fields.py
index cf3fa6916a..b9fa3e9746 100644
--- a/tests/integration/test_host_mode_entity_fields.py
+++ b/tests/integration/test_host_mode_entity_fields.py
@@ -25,8 +25,8 @@ async def test_host_mode_entity_fields(
         # Create a map of entity names to entity info
         entity_map = {}
         for entity in entities[0]:
-            if hasattr(entity, "name"):
-                entity_map[entity.name] = entity
+            # All entities should have a name attribute
+            entity_map[entity.name] = entity
 
         # Test entities that should be visible via API (non-internal)
         visible_test_cases = [
diff --git a/tests/integration/test_host_mode_many_entities.py b/tests/integration/test_host_mode_many_entities.py
index 005728b8c6..19d1ee315f 100644
--- a/tests/integration/test_host_mode_many_entities.py
+++ b/tests/integration/test_host_mode_many_entities.py
@@ -4,7 +4,7 @@ from __future__ import annotations
 
 import asyncio
 
-from aioesphomeapi import EntityState
+from aioesphomeapi import EntityState, SensorState
 import pytest
 
 from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -30,7 +30,7 @@ async def test_host_mode_many_entities(
             sensor_states = [
                 s
                 for s in states.values()
-                if hasattr(s, "state") and isinstance(s.state, float)
+                if isinstance(s, SensorState) and isinstance(s.state, float)
             ]
             # When we have received states from at least 50 sensors, resolve the future
             if len(sensor_states) >= 50 and not sensor_count_future.done():
@@ -45,7 +45,7 @@ async def test_host_mode_many_entities(
             sensor_states = [
                 s
                 for s in states.values()
-                if hasattr(s, "state") and isinstance(s.state, float)
+                if isinstance(s, SensorState) and isinstance(s.state, float)
             ]
             pytest.fail(
                 f"Did not receive states from at least 50 sensors within 10 seconds. "
@@ -61,7 +61,7 @@ async def test_host_mode_many_entities(
         sensor_states = [
             s
             for s in states.values()
-            if hasattr(s, "state") and isinstance(s.state, float)
+            if isinstance(s, SensorState) and isinstance(s.state, float)
         ]
 
         assert sensor_count >= 50, (
diff --git a/tests/integration/test_host_mode_sensor.py b/tests/integration/test_host_mode_sensor.py
index 049f7db619..8c1e9f5d51 100644
--- a/tests/integration/test_host_mode_sensor.py
+++ b/tests/integration/test_host_mode_sensor.py
@@ -19,16 +19,17 @@ async def test_host_mode_with_sensor(
 ) -> None:
     """Test Host mode with a sensor component."""
     # Write, compile and run the ESPHome device, then connect to API
+    loop = asyncio.get_running_loop()
     async with run_compiled(yaml_config), api_client_connected() as client:
         # Subscribe to state changes
         states: dict[int, EntityState] = {}
-        sensor_future: asyncio.Future[EntityState] = asyncio.Future()
+        sensor_future: asyncio.Future[EntityState] = loop.create_future()
 
         def on_state(state: EntityState) -> None:
             states[state.key] = state
             # If this is our sensor with value 42.0, resolve the future
             if (
-                hasattr(state, "state")
+                isinstance(state, aioesphomeapi.SensorState)
                 and state.state == 42.0
                 and not sensor_future.done()
             ):
diff --git a/tests/integration/test_light_calls.py b/tests/integration/test_light_calls.py
index 8ecb77fb99..1c56bbbf9e 100644
--- a/tests/integration/test_light_calls.py
+++ b/tests/integration/test_light_calls.py
@@ -7,6 +7,7 @@ including RGB, color temperature, effects, transitions, and flash.
 import asyncio
 from typing import Any
 
+from aioesphomeapi import LightState
 import pytest
 
 from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -76,7 +77,7 @@ async def test_light_calls(
         client.light_command(key=rgbcw_light.key, white=0.6)
         state = await wait_for_state_change(rgbcw_light.key)
         # White might need more tolerance or might not be directly settable
-        if hasattr(state, "white"):
+        if isinstance(state, LightState) and state.white is not None:
             assert state.white == pytest.approx(0.6, abs=0.1)
 
         # Test 8: color_temperature only

From 256f9f994393a37783043faaedbabac630bda2d4 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Tue, 8 Jul 2025 13:30:23 +1200
Subject: [PATCH 279/293] [helpers] Improve ``format_hex_pretty`` (#9380)

---
 esphome/core/helpers.cpp |  65 ++++++++--------
 esphome/core/helpers.h   | 155 +++++++++++++++++++++++++++++++++++----
 2 files changed, 178 insertions(+), 42 deletions(-)

diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp
index 22b74e11fa..0c11c5d486 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -258,53 +258,60 @@ std::string format_hex(const uint8_t *data, size_t length) {
 std::string format_hex(const std::vector &data) { return format_hex(data.data(), data.size()); }
 
 static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; }
-std::string format_hex_pretty(const uint8_t *data, size_t length) {
-  if (length == 0)
+std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) {
+  if (data == nullptr || length == 0)
     return "";
   std::string ret;
-  ret.resize(3 * length - 1);
+  uint8_t multiple = separator ? 3 : 2;  // 3 if separator is not \0, 2 otherwise
+  ret.resize(multiple * length - 1);
   for (size_t i = 0; i < length; i++) {
-    ret[3 * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
-    ret[3 * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
-    if (i != length - 1)
-      ret[3 * i + 2] = '.';
+    ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
+    ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
+    if (separator && i != length - 1)
+      ret[multiple * i + 2] = separator;
   }
-  if (length > 4)
-    return ret + " (" + to_string(length) + ")";
+  if (show_length && length > 4)
+    return ret + " (" + std::to_string(length) + ")";
   return ret;
 }
-std::string format_hex_pretty(const std::vector &data) { return format_hex_pretty(data.data(), data.size()); }
+std::string format_hex_pretty(const std::vector &data, char separator, bool show_length) {
+  return format_hex_pretty(data.data(), data.size(), separator, show_length);
+}
 
-std::string format_hex_pretty(const uint16_t *data, size_t length) {
-  if (length == 0)
+std::string format_hex_pretty(const uint16_t *data, size_t length, char separator, bool show_length) {
+  if (data == nullptr || length == 0)
     return "";
   std::string ret;
-  ret.resize(5 * length - 1);
+  uint8_t multiple = separator ? 5 : 4;  // 5 if separator is not \0, 4 otherwise
+  ret.resize(multiple * length - 1);
   for (size_t i = 0; i < length; i++) {
-    ret[5 * i] = format_hex_pretty_char((data[i] & 0xF000) >> 12);
-    ret[5 * i + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8);
-    ret[5 * i + 2] = format_hex_pretty_char((data[i] & 0x00F0) >> 4);
-    ret[5 * i + 3] = format_hex_pretty_char(data[i] & 0x000F);
-    if (i != length - 1)
-      ret[5 * i + 2] = '.';
+    ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF000) >> 12);
+    ret[multiple * i + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8);
+    ret[multiple * i + 2] = format_hex_pretty_char((data[i] & 0x00F0) >> 4);
+    ret[multiple * i + 3] = format_hex_pretty_char(data[i] & 0x000F);
+    if (separator && i != length - 1)
+      ret[multiple * i + 4] = separator;
   }
-  if (length > 4)
-    return ret + " (" + to_string(length) + ")";
+  if (show_length && length > 4)
+    return ret + " (" + std::to_string(length) + ")";
   return ret;
 }
-std::string format_hex_pretty(const std::vector &data) { return format_hex_pretty(data.data(), data.size()); }
-std::string format_hex_pretty(const std::string &data) {
+std::string format_hex_pretty(const std::vector &data, char separator, bool show_length) {
+  return format_hex_pretty(data.data(), data.size(), separator, show_length);
+}
+std::string format_hex_pretty(const std::string &data, char separator, bool show_length) {
   if (data.empty())
     return "";
   std::string ret;
-  ret.resize(3 * data.length() - 1);
+  uint8_t multiple = separator ? 3 : 2;  // 3 if separator is not \0, 2 otherwise
+  ret.resize(multiple * data.length() - 1);
   for (size_t i = 0; i < data.length(); i++) {
-    ret[3 * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
-    ret[3 * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
-    if (i != data.length() - 1)
-      ret[3 * i + 2] = '.';
+    ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
+    ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
+    if (separator && i != data.length() - 1)
+      ret[multiple * i + 2] = separator;
   }
-  if (data.length() > 4)
+  if (show_length && data.length() > 4)
     return ret + " (" + std::to_string(data.length()) + ")";
   return ret;
 }
diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h
index d92cf07702..58f162ff9d 100644
--- a/esphome/core/helpers.h
+++ b/esphome/core/helpers.h
@@ -344,20 +344,149 @@ template std::string format_hex(const std::array &dat
   return format_hex(data.data(), data.size());
 }
 
-/// Format the byte array \p data of length \p len in pretty-printed, human-readable hex.
-std::string format_hex_pretty(const uint8_t *data, size_t length);
-/// Format the word array \p data of length \p len in pretty-printed, human-readable hex.
-std::string format_hex_pretty(const uint16_t *data, size_t length);
-/// Format the vector \p data in pretty-printed, human-readable hex.
-std::string format_hex_pretty(const std::vector &data);
-/// Format the vector \p data in pretty-printed, human-readable hex.
-std::string format_hex_pretty(const std::vector &data);
-/// Format the string \p data in pretty-printed, human-readable hex.
-std::string format_hex_pretty(const std::string &data);
-/// Format an unsigned integer in pretty-printed, human-readable hex, starting with the most significant byte.
-template::value, int> = 0> std::string format_hex_pretty(T val) {
+/** Format a byte array in pretty-printed, human-readable hex format.
+ *
+ * Converts binary data to a hexadecimal string representation with customizable formatting.
+ * Each byte is displayed as a two-digit uppercase hex value, separated by the specified separator.
+ * Optionally includes the total byte count in parentheses at the end.
+ *
+ * @param data Pointer to the byte array to format.
+ * @param length Number of bytes in the array.
+ * @param separator Character to use between hex bytes (default: '.').
+ * @param show_length Whether to append the byte count in parentheses (default: true).
+ * @return Formatted hex string, e.g., "A1.B2.C3.D4.E5 (5)" or "A1:B2:C3" depending on parameters.
+ *
+ * @note Returns empty string if data is nullptr or length is 0.
+ * @note The length will only be appended if show_length is true AND the length is greater than 4.
+ *
+ * Example:
+ * @code
+ * uint8_t data[] = {0xA1, 0xB2, 0xC3};
+ * format_hex_pretty(data, 3);           // Returns "A1.B2.C3" (no length shown for <= 4 parts)
+ * uint8_t data2[] = {0xA1, 0xB2, 0xC3, 0xD4, 0xE5};
+ * format_hex_pretty(data2, 5);          // Returns "A1.B2.C3.D4.E5 (5)"
+ * format_hex_pretty(data2, 5, ':');     // Returns "A1:B2:C3:D4:E5 (5)"
+ * format_hex_pretty(data2, 5, '.', false); // Returns "A1.B2.C3.D4.E5"
+ * @endcode
+ */
+std::string format_hex_pretty(const uint8_t *data, size_t length, char separator = '.', bool show_length = true);
+
+/** Format a 16-bit word array in pretty-printed, human-readable hex format.
+ *
+ * Similar to the byte array version, but formats 16-bit words as 4-digit hex values.
+ *
+ * @param data Pointer to the 16-bit word array to format.
+ * @param length Number of 16-bit words in the array.
+ * @param separator Character to use between hex words (default: '.').
+ * @param show_length Whether to append the word count in parentheses (default: true).
+ * @return Formatted hex string with 4-digit hex values per word.
+ *
+ * @note The length will only be appended if show_length is true AND the length is greater than 4.
+ *
+ * Example:
+ * @code
+ * uint16_t data[] = {0xA1B2, 0xC3D4};
+ * format_hex_pretty(data, 2); // Returns "A1B2.C3D4" (no length shown for <= 4 parts)
+ * uint16_t data2[] = {0xA1B2, 0xC3D4, 0xE5F6};
+ * format_hex_pretty(data2, 3); // Returns "A1B2.C3D4.E5F6 (3)"
+ * @endcode
+ */
+std::string format_hex_pretty(const uint16_t *data, size_t length, char separator = '.', bool show_length = true);
+
+/** Format a byte vector in pretty-printed, human-readable hex format.
+ *
+ * Convenience overload for std::vector. Formats each byte as a two-digit
+ * uppercase hex value with customizable separator.
+ *
+ * @param data Vector of bytes to format.
+ * @param separator Character to use between hex bytes (default: '.').
+ * @param show_length Whether to append the byte count in parentheses (default: true).
+ * @return Formatted hex string representation of the vector contents.
+ *
+ * @note The length will only be appended if show_length is true AND the vector size is greater than 4.
+ *
+ * Example:
+ * @code
+ * std::vector data = {0xDE, 0xAD, 0xBE, 0xEF};
+ * format_hex_pretty(data);        // Returns "DE.AD.BE.EF" (no length shown for <= 4 parts)
+ * std::vector data2 = {0xDE, 0xAD, 0xBE, 0xEF, 0xCA};
+ * format_hex_pretty(data2);       // Returns "DE.AD.BE.EF.CA (5)"
+ * format_hex_pretty(data2, '-');  // Returns "DE-AD-BE-EF-CA (5)"
+ * @endcode
+ */
+std::string format_hex_pretty(const std::vector &data, char separator = '.', bool show_length = true);
+
+/** Format a 16-bit word vector in pretty-printed, human-readable hex format.
+ *
+ * Convenience overload for std::vector. Each 16-bit word is formatted
+ * as a 4-digit uppercase hex value in big-endian order.
+ *
+ * @param data Vector of 16-bit words to format.
+ * @param separator Character to use between hex words (default: '.').
+ * @param show_length Whether to append the word count in parentheses (default: true).
+ * @return Formatted hex string representation of the vector contents.
+ *
+ * @note The length will only be appended if show_length is true AND the vector size is greater than 4.
+ *
+ * Example:
+ * @code
+ * std::vector data = {0x1234, 0x5678};
+ * format_hex_pretty(data); // Returns "1234.5678" (no length shown for <= 4 parts)
+ * std::vector data2 = {0x1234, 0x5678, 0x9ABC};
+ * format_hex_pretty(data2); // Returns "1234.5678.9ABC (3)"
+ * @endcode
+ */
+std::string format_hex_pretty(const std::vector &data, char separator = '.', bool show_length = true);
+
+/** Format a string's bytes in pretty-printed, human-readable hex format.
+ *
+ * Treats each character in the string as a byte and formats it in hex.
+ * Useful for debugging binary data stored in std::string containers.
+ *
+ * @param data String whose bytes should be formatted as hex.
+ * @param separator Character to use between hex bytes (default: '.').
+ * @param show_length Whether to append the byte count in parentheses (default: true).
+ * @return Formatted hex string representation of the string's byte contents.
+ *
+ * @note The length will only be appended if show_length is true AND the string length is greater than 4.
+ *
+ * Example:
+ * @code
+ * std::string data = "ABC";  // ASCII: 0x41, 0x42, 0x43
+ * format_hex_pretty(data);   // Returns "41.42.43" (no length shown for <= 4 parts)
+ * std::string data2 = "ABCDE";
+ * format_hex_pretty(data2);  // Returns "41.42.43.44.45 (5)"
+ * @endcode
+ */
+std::string format_hex_pretty(const std::string &data, char separator = '.', bool show_length = true);
+
+/** Format an unsigned integer in pretty-printed, human-readable hex format.
+ *
+ * Converts the integer to big-endian byte order and formats each byte as hex.
+ * The most significant byte appears first in the output string.
+ *
+ * @tparam T Unsigned integer type (uint8_t, uint16_t, uint32_t, uint64_t, etc.).
+ * @param val The unsigned integer value to format.
+ * @param separator Character to use between hex bytes (default: '.').
+ * @param show_length Whether to append the byte count in parentheses (default: true).
+ * @return Formatted hex string with most significant byte first.
+ *
+ * @note The length will only be appended if show_length is true AND sizeof(T) is greater than 4.
+ *
+ * Example:
+ * @code
+ * uint32_t value = 0x12345678;
+ * format_hex_pretty(value);        // Returns "12.34.56.78" (no length shown for <= 4 parts)
+ * uint64_t value2 = 0x123456789ABCDEF0;
+ * format_hex_pretty(value2);       // Returns "12.34.56.78.9A.BC.DE.F0 (8)"
+ * format_hex_pretty(value2, ':');  // Returns "12:34:56:78:9A:BC:DE:F0 (8)"
+ * format_hex_pretty(0x1234); // Returns "12.34"
+ * @endcode
+ */
+template::value, int> = 0>
+std::string format_hex_pretty(T val, char separator = '.', bool show_length = true) {
   val = convert_big_endian(val);
-  return format_hex_pretty(reinterpret_cast(&val), sizeof(T));
+  return format_hex_pretty(reinterpret_cast(&val), sizeof(T), separator, show_length);
 }
 
 /// Format the byte array \p data of length \p len in binary.

From 51377b26253d865fd071df0188948a7f932654fb Mon Sep 17 00:00:00 2001
From: functionpointer 
Date: Tue, 8 Jul 2025 04:27:33 +0200
Subject: [PATCH 280/293] hydreon_rgxx: remove precipitation_intensity from RG9
 (#9367)

---
 esphome/components/hydreon_rgxx/sensor.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py
index f81703c087..6c9f1e2877 100644
--- a/esphome/components/hydreon_rgxx/sensor.py
+++ b/esphome/components/hydreon_rgxx/sensor.py
@@ -111,8 +111,8 @@ CONFIG_SCHEMA = cv.All(
             cv.Optional(CONF_MOISTURE): sensor.sensor_schema(
                 unit_of_measurement=UNIT_INTENSITY,
                 accuracy_decimals=0,
-                device_class=DEVICE_CLASS_PRECIPITATION_INTENSITY,
                 state_class=STATE_CLASS_MEASUREMENT,
+                icon="mdi:weather-rainy",
             ),
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
                 unit_of_measurement=UNIT_CELSIUS,

From 4648804db6843e997bd04f232f39941ec9384d94 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 8 Jul 2025 12:28:00 +1000
Subject: [PATCH 281/293] [image] Add byte order option and unit tests (#9326)

---
 esphome/components/const/__init__.py          |   1 +
 esphome/components/image/__init__.py          | 147 +++++++++++---
 tests/component_tests/conftest.py             |  54 +++++-
 tests/component_tests/image/config/bad.png    |   0
 tests/component_tests/image/config/image.png  | Bin 0 -> 685 bytes
 .../image/config/image_test.yaml              |  20 ++
 tests/component_tests/image/test_init.py      | 183 ++++++++++++++++++
 tests/components/image/test.esp32-ard.yaml    |  17 --
 tests/components/image/test.esp32-c3-ard.yaml |  16 --
 tests/components/image/test.esp32-c3-idf.yaml |  16 --
 tests/components/image/test.esp8266-ard.yaml  |  11 +-
 11 files changed, 385 insertions(+), 80 deletions(-)
 create mode 100644 tests/component_tests/image/config/bad.png
 create mode 100644 tests/component_tests/image/config/image.png
 create mode 100644 tests/component_tests/image/config/image_test.yaml
 create mode 100644 tests/component_tests/image/test_init.py
 delete mode 100644 tests/components/image/test.esp32-ard.yaml
 delete mode 100644 tests/components/image/test.esp32-c3-ard.yaml
 delete mode 100644 tests/components/image/test.esp32-c3-idf.yaml

diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py
index 66a5fe5d81..b084622f4c 100644
--- a/esphome/components/const/__init__.py
+++ b/esphome/components/const/__init__.py
@@ -2,6 +2,7 @@
 
 CODEOWNERS = ["@esphome/core"]
 
+CONF_BYTE_ORDER = "byte_order"
 CONF_DRAW_ROUNDING = "draw_rounding"
 CONF_ON_STATE_CHANGE = "on_state_change"
 CONF_REQUEST_HEADERS = "request_headers"
diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py
index 5d593ac3d4..f6d8673a08 100644
--- a/esphome/components/image/__init__.py
+++ b/esphome/components/image/__init__.py
@@ -10,8 +10,10 @@ from PIL import Image, UnidentifiedImageError
 
 from esphome import core, external_files
 import esphome.codegen as cg
+from esphome.components.const import CONF_BYTE_ORDER
 import esphome.config_validation as cv
 from esphome.const import (
+    CONF_DEFAULTS,
     CONF_DITHER,
     CONF_FILE,
     CONF_ICON,
@@ -38,6 +40,7 @@ CONF_OPAQUE = "opaque"
 CONF_CHROMA_KEY = "chroma_key"
 CONF_ALPHA_CHANNEL = "alpha_channel"
 CONF_INVERT_ALPHA = "invert_alpha"
+CONF_IMAGES = "images"
 
 TRANSPARENCY_TYPES = (
     CONF_OPAQUE,
@@ -188,6 +191,10 @@ class ImageRGB565(ImageEncoder):
             dither,
             invert_alpha,
         )
+        self.big_endian = True
+
+    def set_big_endian(self, big_endian: bool) -> None:
+        self.big_endian = big_endian
 
     def convert(self, image, path):
         return image.convert("RGBA")
@@ -205,10 +212,16 @@ class ImageRGB565(ImageEncoder):
                 g = 1
                 b = 0
         rgb = (r << 11) | (g << 5) | b
-        self.data[self.index] = rgb >> 8
-        self.index += 1
-        self.data[self.index] = rgb & 0xFF
-        self.index += 1
+        if self.big_endian:
+            self.data[self.index] = rgb >> 8
+            self.index += 1
+            self.data[self.index] = rgb & 0xFF
+            self.index += 1
+        else:
+            self.data[self.index] = rgb & 0xFF
+            self.index += 1
+            self.data[self.index] = rgb >> 8
+            self.index += 1
         if self.transparency == CONF_ALPHA_CHANNEL:
             if self.invert_alpha:
                 a ^= 0xFF
@@ -364,7 +377,7 @@ def validate_file_shorthand(value):
     value = cv.string_strict(value)
     parts = value.strip().split(":")
     if len(parts) == 2 and parts[0] in MDI_SOURCES:
-        match = re.match(r"[a-zA-Z0-9\-]+", parts[1])
+        match = re.match(r"^[a-zA-Z0-9\-]+$", parts[1])
         if match is None:
             raise cv.Invalid(f"Could not parse mdi icon name from '{value}'.")
         return download_gh_svg(parts[1], parts[0])
@@ -434,20 +447,29 @@ def validate_type(image_types):
 
 
 def validate_settings(value):
-    type = value[CONF_TYPE]
+    """
+    Validate the settings for a single image configuration.
+    """
+    conf_type = value[CONF_TYPE]
+    type_class = IMAGE_TYPE[conf_type]
     transparency = value[CONF_TRANSPARENCY].lower()
-    allow_config = IMAGE_TYPE[type].allow_config
-    if transparency not in allow_config:
+    if transparency not in type_class.allow_config:
         raise cv.Invalid(
-            f"Image format '{type}' cannot have transparency: {transparency}"
+            f"Image format '{conf_type}' cannot have transparency: {transparency}"
         )
     invert_alpha = value.get(CONF_INVERT_ALPHA, False)
     if (
         invert_alpha
         and transparency != CONF_ALPHA_CHANNEL
-        and CONF_INVERT_ALPHA not in allow_config
+        and CONF_INVERT_ALPHA not in type_class.allow_config
     ):
         raise cv.Invalid("No alpha channel to invert")
+    if value.get(CONF_BYTE_ORDER) is not None and not callable(
+        getattr(type_class, "set_big_endian", None)
+    ):
+        raise cv.Invalid(
+            f"Image format '{conf_type}' does not support byte order configuration"
+        )
     if file := value.get(CONF_FILE):
         file = Path(file)
         if is_svg_file(file):
@@ -456,31 +478,82 @@ def validate_settings(value):
             try:
                 Image.open(file)
             except UnidentifiedImageError as exc:
-                raise cv.Invalid(f"File can't be opened as image: {file}") from exc
+                raise cv.Invalid(
+                    f"File can't be opened as image: {file.absolute()}"
+                ) from exc
     return value
 
 
+IMAGE_ID_SCHEMA = {
+    cv.Required(CONF_ID): cv.declare_id(Image_),
+    cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
+    cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
+}
+
+
+OPTIONS_SCHEMA = {
+    cv.Optional(CONF_RESIZE): cv.dimensions,
+    cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
+        "NONE", "FLOYDSTEINBERG", upper=True
+    ),
+    cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
+    cv.Optional(CONF_BYTE_ORDER): cv.one_of("BIG_ENDIAN", "LITTLE_ENDIAN", upper=True),
+    cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
+    cv.Optional(CONF_TYPE): validate_type(IMAGE_TYPE),
+}
+
+OPTIONS = [key.schema for key in OPTIONS_SCHEMA]
+
+# image schema with no defaults, used with `CONF_IMAGES` in the config
+IMAGE_SCHEMA_NO_DEFAULTS = {
+    **IMAGE_ID_SCHEMA,
+    **{cv.Optional(key): OPTIONS_SCHEMA[key] for key in OPTIONS},
+}
+
 BASE_SCHEMA = cv.Schema(
     {
-        cv.Required(CONF_ID): cv.declare_id(Image_),
-        cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
-        cv.Optional(CONF_RESIZE): cv.dimensions,
-        cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
-            "NONE", "FLOYDSTEINBERG", upper=True
-        ),
-        cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
-        cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
+        **IMAGE_ID_SCHEMA,
+        **OPTIONS_SCHEMA,
     }
 ).add_extra(validate_settings)
 
 IMAGE_SCHEMA = BASE_SCHEMA.extend(
     {
         cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
-        cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
     }
 )
 
 
+def validate_defaults(value):
+    """
+    Validate the options for images with defaults
+    """
+    defaults = value[CONF_DEFAULTS]
+    result = []
+    for index, image in enumerate(value[CONF_IMAGES]):
+        type = image.get(CONF_TYPE, defaults.get(CONF_TYPE))
+        if type is None:
+            raise cv.Invalid(
+                "Type is required either in the image config or in the defaults",
+                path=[CONF_IMAGES, index],
+            )
+        type_class = IMAGE_TYPE[type]
+        # A default byte order should be simply ignored if the type does not support it
+        available_options = [*OPTIONS]
+        if (
+            not callable(getattr(type_class, "set_big_endian", None))
+            and CONF_BYTE_ORDER not in image
+        ):
+            available_options.remove(CONF_BYTE_ORDER)
+        config = {
+            **{key: image.get(key, defaults.get(key)) for key in available_options},
+            **{key.schema: image[key.schema] for key in IMAGE_ID_SCHEMA},
+        }
+        validate_settings(config)
+        result.append(config)
+    return result
+
+
 def typed_image_schema(image_type):
     """
     Construct a schema for a specific image type, allowing transparency options
@@ -523,10 +596,33 @@ def typed_image_schema(image_type):
 
 # The config schema can be a (possibly empty) single list of images,
 # or a dictionary of image types each with a list of images
-CONFIG_SCHEMA = cv.Any(
-    cv.Schema({cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}),
-    cv.ensure_list(IMAGE_SCHEMA),
-)
+# or a dictionary with keys `defaults:` and `images:`
+
+
+def _config_schema(config):
+    if isinstance(config, list):
+        return cv.Schema([IMAGE_SCHEMA])(config)
+    if not isinstance(config, dict):
+        raise cv.Invalid(
+            "Badly formed image configuration, expected a list or a dictionary"
+        )
+    if CONF_DEFAULTS in config or CONF_IMAGES in config:
+        return validate_defaults(
+            cv.Schema(
+                {
+                    cv.Required(CONF_DEFAULTS): OPTIONS_SCHEMA,
+                    cv.Required(CONF_IMAGES): cv.ensure_list(IMAGE_SCHEMA_NO_DEFAULTS),
+                }
+            )(config)
+        )
+    if CONF_ID in config or CONF_FILE in config:
+        return cv.ensure_list(IMAGE_SCHEMA)([config])
+    return cv.Schema(
+        {cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}
+    )(config)
+
+
+CONFIG_SCHEMA = _config_schema
 
 
 async def write_image(config, all_frames=False):
@@ -585,6 +681,9 @@ async def write_image(config, all_frames=False):
 
     total_rows = height * frame_count
     encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
+    if byte_order := config.get(CONF_BYTE_ORDER):
+        # Check for valid type has already been done in validate_settings
+        encoder.set_big_endian(byte_order == "BIG_ENDIAN")
     for frame_index in range(frame_count):
         image.seek(frame_index)
         pixels = encoder.convert(image.resize((width, height)), path).getdata()
diff --git a/tests/component_tests/conftest.py b/tests/component_tests/conftest.py
index 7aa7dfe698..b1e0eaa200 100644
--- a/tests/component_tests/conftest.py
+++ b/tests/component_tests/conftest.py
@@ -1,29 +1,71 @@
 """Fixtures for component tests."""
 
+from __future__ import annotations
+
+from collections.abc import Callable, Generator
 from pathlib import Path
 import sys
 
+import pytest
+
 # Add package root to python path
 here = Path(__file__).parent
 package_root = here.parent.parent
 sys.path.insert(0, package_root.as_posix())
 
-import pytest  # noqa: E402
-
 from esphome.__main__ import generate_cpp_contents  # noqa: E402
 from esphome.config import read_config  # noqa: E402
 from esphome.core import CORE  # noqa: E402
 
 
+@pytest.fixture(autouse=True)
+def config_path(request: pytest.FixtureRequest) -> Generator[None]:
+    """Set CORE.config_path to the component's config directory and reset it after the test."""
+    original_path = CORE.config_path
+    config_dir = Path(request.fspath).parent / "config"
+
+    # Check if config directory exists, if not use parent directory
+    if config_dir.exists():
+        # Set config_path to a dummy yaml file in the config directory
+        # This ensures CORE.config_dir points to the config directory
+        CORE.config_path = str(config_dir / "dummy.yaml")
+    else:
+        CORE.config_path = str(Path(request.fspath).parent / "dummy.yaml")
+
+    yield
+    CORE.config_path = original_path
+
+
 @pytest.fixture
-def generate_main():
+def component_fixture_path(request: pytest.FixtureRequest) -> Callable[[str], Path]:
+    """Return a function to get absolute paths relative to the component's fixtures directory."""
+
+    def _get_path(file_name: str) -> Path:
+        """Get the absolute path of a file relative to the component's fixtures directory."""
+        return (Path(request.fspath).parent / "fixtures" / file_name).absolute()
+
+    return _get_path
+
+
+@pytest.fixture
+def component_config_path(request: pytest.FixtureRequest) -> Callable[[str], Path]:
+    """Return a function to get absolute paths relative to the component's config directory."""
+
+    def _get_path(file_name: str) -> Path:
+        """Get the absolute path of a file relative to the component's config directory."""
+        return (Path(request.fspath).parent / "config" / file_name).absolute()
+
+    return _get_path
+
+
+@pytest.fixture
+def generate_main() -> Generator[Callable[[str | Path], str]]:
     """Generates the C++ main.cpp file and returns it in string form."""
 
-    def generator(path: str) -> str:
-        CORE.config_path = path
+    def generator(path: str | Path) -> str:
+        CORE.config_path = str(path)
         CORE.config = read_config({})
         generate_cpp_contents(CORE.config)
-        print(CORE.cpp_main_section)
         return CORE.cpp_main_section
 
     yield generator
diff --git a/tests/component_tests/image/config/bad.png b/tests/component_tests/image/config/bad.png
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/component_tests/image/config/image.png b/tests/component_tests/image/config/image.png
new file mode 100644
index 0000000000000000000000000000000000000000..bd2fd547833635ab465a796c9a936dcfdf4b3086
GIT binary patch
literal 685
zcmV;e0#f~nP)L}=x{JFG>CwJ5D;t#m}DrRq)32-m{@Rt7*HUfm=IXNV4$eL@SH$^Fd$%z
zXjoi`ptN`(B!GB`_=uRmD2yPGoKSFNfG~g{NN8A)6cAu&aBz5tz+h+?v>2eMc$h38
zh?J<9ps=W{IB0lqsBAEBj1W*HOPAS56LM;hJ$00DtXL_t(2&#jVOQxicDMK?xJC#$kXC61eE
z6`~?!1rcMW6~E$=i|7CU9v?PBQe~Ad_RHLwI(2VV_bvFx6#oSfDDRv@7RX2~?N{N(R%R&zd#e0wv(eX6D$m5o=GAMo^6d3z%Q42eiWHL=s66(FHVl1vh@
zdrbP{5G*WCPwlS1d)wa8-m~yf*L6s!a$=Nc`=WEBx
z8I|OWAmdKXc)1oaghWmtI0r$31bGE0NI~q2eOHhcQXqwmK5$jDb*=T%ioeZobIrF8
T1OmU^00000NkvXXu0mjf None:
+    """Test detection of invalid configuration."""
+    with pytest.raises(cv.Invalid, match=error_match):
+        CONFIG_SCHEMA(config)
+
+
+@pytest.mark.parametrize(
+    "config",
+    [
+        pytest.param(
+            {
+                "id": "image_id",
+                "file": "image.png",
+                "type": "rgb565",
+                "transparency": "chroma_key",
+                "byte_order": "little_endian",
+                "dither": "FloydSteinberg",
+                "resize": "100x100",
+                "invert_alpha": False,
+            },
+            id="single_image_all_options",
+        ),
+        pytest.param(
+            [
+                {
+                    "id": "image_id",
+                    "file": "image.png",
+                    "type": "binary",
+                }
+            ],
+            id="list_of_images",
+        ),
+        pytest.param(
+            {
+                "defaults": {
+                    "type": "rgb565",
+                    "transparency": "chroma_key",
+                    "byte_order": "little_endian",
+                    "dither": "FloydSteinberg",
+                    "resize": "100x100",
+                    "invert_alpha": False,
+                },
+                "images": [
+                    {
+                        "id": "image_id",
+                        "file": "image.png",
+                    }
+                ],
+            },
+            id="images_with_defaults",
+        ),
+        pytest.param(
+            {
+                "rgb565": {
+                    "alpha_channel": [
+                        {
+                            "id": "image_id",
+                            "file": "image.png",
+                            "transparency": "alpha_channel",
+                            "byte_order": "little_endian",
+                            "dither": "FloydSteinberg",
+                            "resize": "100x100",
+                            "invert_alpha": False,
+                        }
+                    ]
+                },
+                "binary": [
+                    {
+                        "id": "image_id",
+                        "file": "image.png",
+                        "transparency": "opaque",
+                        "dither": "FloydSteinberg",
+                        "resize": "100x100",
+                        "invert_alpha": False,
+                    }
+                ],
+            },
+            id="type_based_organization",
+        ),
+    ],
+)
+def test_image_configuration_success(
+    config: dict[str, Any] | list[dict[str, Any]],
+) -> None:
+    """Test successful configuration validation."""
+    CONFIG_SCHEMA(config)
+
+
+def test_image_generation(
+    generate_main: Callable[[str | Path], str],
+    component_config_path: Callable[[str], Path],
+) -> None:
+    """Test image generation configuration."""
+
+    main_cpp = generate_main(component_config_path("image_test.yaml"))
+    assert "uint8_t_id[] PROGMEM = {0x24, 0x21, 0x24, 0x21" in main_cpp
+    assert (
+        "cat_img = new image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);"
+        in main_cpp
+    )
diff --git a/tests/components/image/test.esp32-ard.yaml b/tests/components/image/test.esp32-ard.yaml
deleted file mode 100644
index 818e720221..0000000000
--- a/tests/components/image/test.esp32-ard.yaml
+++ /dev/null
@@ -1,17 +0,0 @@
-spi:
-  - id: spi_main_lcd
-    clk_pin: 16
-    mosi_pin: 17
-    miso_pin: 32
-
-display:
-  - platform: ili9xxx
-    id: main_lcd
-    model: ili9342
-    cs_pin: 14
-    dc_pin: 13
-    reset_pin: 21
-    invert_colors: true
-
-<<: !include common.yaml
-
diff --git a/tests/components/image/test.esp32-c3-ard.yaml b/tests/components/image/test.esp32-c3-ard.yaml
deleted file mode 100644
index 4dae9cd5ec..0000000000
--- a/tests/components/image/test.esp32-c3-ard.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-spi:
-  - id: spi_main_lcd
-    clk_pin: 6
-    mosi_pin: 7
-    miso_pin: 5
-
-display:
-  - platform: ili9xxx
-    id: main_lcd
-    model: ili9342
-    cs_pin: 3
-    dc_pin: 11
-    reset_pin: 10
-    invert_colors: true
-
-<<: !include common.yaml
diff --git a/tests/components/image/test.esp32-c3-idf.yaml b/tests/components/image/test.esp32-c3-idf.yaml
deleted file mode 100644
index 4dae9cd5ec..0000000000
--- a/tests/components/image/test.esp32-c3-idf.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-spi:
-  - id: spi_main_lcd
-    clk_pin: 6
-    mosi_pin: 7
-    miso_pin: 5
-
-display:
-  - platform: ili9xxx
-    id: main_lcd
-    model: ili9342
-    cs_pin: 3
-    dc_pin: 11
-    reset_pin: 10
-    invert_colors: true
-
-<<: !include common.yaml
diff --git a/tests/components/image/test.esp8266-ard.yaml b/tests/components/image/test.esp8266-ard.yaml
index f963022ff4..626076d44e 100644
--- a/tests/components/image/test.esp8266-ard.yaml
+++ b/tests/components/image/test.esp8266-ard.yaml
@@ -13,4 +13,13 @@ display:
     reset_pin: 16
     invert_colors: true
 
-<<: !include common.yaml
+image:
+  defaults:
+    type: rgb565
+    transparency: opaque
+    byte_order: little_endian
+    resize: 50x50
+    dither: FloydSteinberg
+  images:
+    - id: test_image
+      file: ../../pnglogo.png

From a30f01d66851912bd1689d848e6725bddfe1ab61 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Mon, 7 Jul 2025 23:34:39 -0500
Subject: [PATCH 282/293] Fix integration test race condition by isolating
 PlatformIO directories (#9383)

---
 tests/integration/conftest.py | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
index 8f5f77ca52..aead6a73af 100644
--- a/tests/integration/conftest.py
+++ b/tests/integration/conftest.py
@@ -165,6 +165,19 @@ async def compile_esphome(
     """Compile an ESPHome configuration and return the binary path."""
 
     async def _compile(config_path: Path) -> Path:
+        # Create a unique PlatformIO directory for this test to avoid race conditions
+        platformio_dir = integration_test_dir / ".platformio"
+        platformio_dir.mkdir(parents=True, exist_ok=True)
+
+        # Create cache directory as well
+        platformio_cache_dir = platformio_dir / ".cache"
+        platformio_cache_dir.mkdir(parents=True, exist_ok=True)
+
+        # Set up environment with isolated PlatformIO directories
+        env = os.environ.copy()
+        env["PLATFORMIO_CORE_DIR"] = str(platformio_dir)
+        env["PLATFORMIO_CACHE_DIR"] = str(platformio_cache_dir)
+
         # Retry compilation up to 3 times if we get a segfault
         max_retries = 3
         for attempt in range(max_retries):
@@ -179,6 +192,7 @@ async def compile_esphome(
                 stdin=asyncio.subprocess.DEVNULL,
                 # Start in a new process group to isolate signal handling
                 start_new_session=True,
+                env=env,
             )
             await proc.wait()
 

From 3f8b691c3295cb598486c6cd2027cad524201893 Mon Sep 17 00:00:00 2001
From: tmpeh <41875356+tmpeh@users.noreply.github.com>
Date: Tue, 8 Jul 2025 06:39:07 +0200
Subject: [PATCH 283/293] Fix format string error in waveshare_epaper.cpp
 (#9322)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/waveshare_epaper/waveshare_epaper.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp
index 575234e780..75c6b84b79 100644
--- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp
+++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp
@@ -3132,7 +3132,7 @@ void HOT GDEY0583T81::display() {
   } else {
     // Partial out (PTOUT), makes the display exit partial mode
     this->command(0x92);
-    ESP_LOGD(TAG, "Partial update done, next full update after %d cycles",
+    ESP_LOGD(TAG, "Partial update done, next full update after %" PRIu32 " cycles",
              this->full_update_every_ - this->at_update_ - 1);
   }
 

From b3d7f001af24a28e2820b173a26114d4c958e600 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" 
Date: Tue, 8 Jul 2025 06:54:47 -0500
Subject: [PATCH 284/293] Fix race condition in scheduler string lifetime
 integration test (#9382)

---
 .../string_lifetime_component.cpp                               | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp
index d377c1fe57..ea386881b2 100644
--- a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp
+++ b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp
@@ -26,7 +26,6 @@ void SchedulerStringLifetimeComponent::run_string_lifetime_test() {
 
   // Schedule final check
   this->set_timeout("final_check", 200, [this]() {
-    ESP_LOGI(TAG, "String lifetime tests complete");
     ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_);
     ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_);
 
@@ -35,6 +34,7 @@ void SchedulerStringLifetimeComponent::run_string_lifetime_test() {
     } else {
       ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_);
     }
+    ESP_LOGI(TAG, "String lifetime tests complete");
   });
 }
 

From 691cc5f7dc2d8f3dcd98b541551262c28d9f7994 Mon Sep 17 00:00:00 2001
From: Simonas Kazlauskas 
Date: Wed, 9 Jul 2025 00:13:58 +0300
Subject: [PATCH 285/293] lps22: add a component (#7540)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Keith Burzinski 
---
 CODEOWNERS                                    |  1 +
 esphome/components/lps22/__init__.py          |  0
 esphome/components/lps22/lps22.cpp            | 75 +++++++++++++++++++
 esphome/components/lps22/lps22.h              | 27 +++++++
 esphome/components/lps22/sensor.py            | 58 ++++++++++++++
 tests/components/lps22/common.yaml            |  8 ++
 tests/components/lps22/test.esp32-ard.yaml    |  6 ++
 tests/components/lps22/test.esp32-c3-ard.yaml |  6 ++
 tests/components/lps22/test.esp32-c3-idf.yaml |  6 ++
 tests/components/lps22/test.esp32-idf.yaml    |  6 ++
 tests/components/lps22/test.esp8266-ard.yaml  |  6 ++
 tests/components/lps22/test.rp2040-ard.yaml   |  6 ++
 12 files changed, 205 insertions(+)
 create mode 100644 esphome/components/lps22/__init__.py
 create mode 100644 esphome/components/lps22/lps22.cpp
 create mode 100644 esphome/components/lps22/lps22.h
 create mode 100644 esphome/components/lps22/sensor.py
 create mode 100644 tests/components/lps22/common.yaml
 create mode 100644 tests/components/lps22/test.esp32-ard.yaml
 create mode 100644 tests/components/lps22/test.esp32-c3-ard.yaml
 create mode 100644 tests/components/lps22/test.esp32-c3-idf.yaml
 create mode 100644 tests/components/lps22/test.esp32-idf.yaml
 create mode 100644 tests/components/lps22/test.esp8266-ard.yaml
 create mode 100644 tests/components/lps22/test.rp2040-ard.yaml

diff --git a/CODEOWNERS b/CODEOWNERS
index ca3849eb0d..faece3fdd7 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -254,6 +254,7 @@ esphome/components/ln882x/* @lamauny
 esphome/components/lock/* @esphome/core
 esphome/components/logger/* @esphome/core
 esphome/components/logger/select/* @clydebarrow
+esphome/components/lps22/* @nagisa
 esphome/components/ltr390/* @latonita @sjtrny
 esphome/components/ltr501/* @latonita
 esphome/components/ltr_als_ps/* @latonita
diff --git a/esphome/components/lps22/__init__.py b/esphome/components/lps22/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/lps22/lps22.cpp b/esphome/components/lps22/lps22.cpp
new file mode 100644
index 0000000000..526286ba72
--- /dev/null
+++ b/esphome/components/lps22/lps22.cpp
@@ -0,0 +1,75 @@
+#include "lps22.h"
+
+namespace esphome {
+namespace lps22 {
+
+static constexpr const char *const TAG = "lps22";
+
+static constexpr uint8_t WHO_AM_I = 0x0F;
+static constexpr uint8_t LPS22HB_ID = 0xB1;
+static constexpr uint8_t LPS22HH_ID = 0xB3;
+static constexpr uint8_t CTRL_REG2 = 0x11;
+static constexpr uint8_t CTRL_REG2_ONE_SHOT_MASK = 0b1;
+static constexpr uint8_t STATUS = 0x27;
+static constexpr uint8_t STATUS_T_DA_MASK = 0b10;
+static constexpr uint8_t STATUS_P_DA_MASK = 0b01;
+static constexpr uint8_t TEMP_L = 0x2b;
+static constexpr uint8_t PRES_OUT_XL = 0x28;
+static constexpr uint8_t REF_P_XL = 0x28;
+static constexpr uint8_t READ_ATTEMPTS = 10;
+static constexpr uint8_t READ_INTERVAL = 5;
+static constexpr float PRESSURE_SCALE = 1.0f / 4096.0f;
+static constexpr float TEMPERATURE_SCALE = 0.01f;
+
+void LPS22Component::setup() {
+  uint8_t value = 0x00;
+  this->read_register(WHO_AM_I, &value, 1);
+  if (value != LPS22HB_ID && value != LPS22HH_ID) {
+    ESP_LOGW(TAG, "device IDs as %02x, which isn't a known LPS22HB or LPS22HH ID", value);
+    this->mark_failed();
+  }
+}
+
+void LPS22Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "LPS22:");
+  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
+  LOG_SENSOR("  ", "Pressure", this->pressure_sensor_);
+  LOG_I2C_DEVICE(this);
+  LOG_UPDATE_INTERVAL(this);
+}
+
+void LPS22Component::update() {
+  uint8_t value = 0x00;
+  this->read_register(CTRL_REG2, &value, 1);
+  value |= CTRL_REG2_ONE_SHOT_MASK;
+  this->write_register(CTRL_REG2, &value, 1);
+  this->set_retry(READ_INTERVAL, READ_ATTEMPTS, [this](uint8_t _) { return this->try_read_(); });
+}
+
+RetryResult LPS22Component::try_read_() {
+  uint8_t value = 0x00;
+  this->read_register(STATUS, &value, 1);
+  const uint8_t expected_status_mask = STATUS_T_DA_MASK | STATUS_P_DA_MASK;
+  if ((value & expected_status_mask) != expected_status_mask) {
+    ESP_LOGD(TAG, "STATUS not ready: %x", value);
+    return RetryResult::RETRY;
+  }
+
+  if (this->temperature_sensor_ != nullptr) {
+    uint8_t t_buf[2]{0};
+    this->read_register(TEMP_L, t_buf, 2);
+    int16_t encoded = static_cast(encode_uint16(t_buf[1], t_buf[0]));
+    float temp = TEMPERATURE_SCALE * static_cast(encoded);
+    this->temperature_sensor_->publish_state(temp);
+  }
+  if (this->pressure_sensor_ != nullptr) {
+    uint8_t p_buf[3]{0};
+    this->read_register(PRES_OUT_XL, p_buf, 3);
+    uint32_t p_lsb = encode_uint24(p_buf[2], p_buf[1], p_buf[0]);
+    this->pressure_sensor_->publish_state(PRESSURE_SCALE * static_cast(p_lsb));
+  }
+  return RetryResult::DONE;
+}
+
+}  // namespace lps22
+}  // namespace esphome
diff --git a/esphome/components/lps22/lps22.h b/esphome/components/lps22/lps22.h
new file mode 100644
index 0000000000..549ea524ea
--- /dev/null
+++ b/esphome/components/lps22/lps22.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/i2c/i2c.h"
+
+namespace esphome {
+namespace lps22 {
+
+class LPS22Component : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
+ public:
+  void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
+  void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; }
+
+  void setup() override;
+  void update() override;
+  void dump_config() override;
+
+ protected:
+  sensor::Sensor *temperature_sensor_{nullptr};
+  sensor::Sensor *pressure_sensor_{nullptr};
+
+  RetryResult try_read_();
+};
+
+}  // namespace lps22
+}  // namespace esphome
diff --git a/esphome/components/lps22/sensor.py b/esphome/components/lps22/sensor.py
new file mode 100644
index 0000000000..87a2106308
--- /dev/null
+++ b/esphome/components/lps22/sensor.py
@@ -0,0 +1,58 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import i2c, sensor
+from esphome.const import (
+    CONF_ID,
+    CONF_TEMPERATURE,
+    CONF_PRESSURE,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+    UNIT_HECTOPASCAL,
+    ICON_THERMOMETER,
+    DEVICE_CLASS_TEMPERATURE,
+    DEVICE_CLASS_PRESSURE,
+)
+
+CODEOWNERS = ["@nagisa"]
+DEPENDENCIES = ["i2c"]
+
+lps22 = cg.esphome_ns.namespace("lps22")
+
+LPS22Component = lps22.class_("LPS22Component", cg.PollingComponent, i2c.I2CDevice)
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(LPS22Component),
+            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_CELSIUS,
+                icon=ICON_THERMOMETER,
+                accuracy_decimals=2,
+                device_class=DEVICE_CLASS_TEMPERATURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_HECTOPASCAL,
+                accuracy_decimals=2,
+                device_class=DEVICE_CLASS_PRESSURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+        }
+    )
+    .extend(cv.polling_component_schema("60s"))
+    .extend(i2c.i2c_device_schema(0x5D))  # can also be 0x5C
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
+
+    if temperature_config := config.get(CONF_TEMPERATURE):
+        sens = await sensor.new_sensor(temperature_config)
+        cg.add(var.set_temperature_sensor(sens))
+
+    if pressure_config := config.get(CONF_PRESSURE):
+        sens = await sensor.new_sensor(pressure_config)
+        cg.add(var.set_pressure_sensor(sens))
diff --git a/tests/components/lps22/common.yaml b/tests/components/lps22/common.yaml
new file mode 100644
index 0000000000..e6de4752ba
--- /dev/null
+++ b/tests/components/lps22/common.yaml
@@ -0,0 +1,8 @@
+sensor:
+  - platform: lps22
+    address: 0x5d
+    update_interval: 10s
+    temperature:
+      name: "LPS22 Temperature"
+    pressure:
+      name: "LPS22 Pressure"
diff --git a/tests/components/lps22/test.esp32-ard.yaml b/tests/components/lps22/test.esp32-ard.yaml
new file mode 100644
index 0000000000..0da6a9577e
--- /dev/null
+++ b/tests/components/lps22/test.esp32-ard.yaml
@@ -0,0 +1,6 @@
+i2c:
+  - id: i2c_lps22
+    scl: 16
+    sda: 17
+
+<<: !include common.yaml
diff --git a/tests/components/lps22/test.esp32-c3-ard.yaml b/tests/components/lps22/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..6091393d31
--- /dev/null
+++ b/tests/components/lps22/test.esp32-c3-ard.yaml
@@ -0,0 +1,6 @@
+i2c:
+  - id: i2c_lps22
+    scl: 5
+    sda: 4
+
+<<: !include common.yaml
diff --git a/tests/components/lps22/test.esp32-c3-idf.yaml b/tests/components/lps22/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..6091393d31
--- /dev/null
+++ b/tests/components/lps22/test.esp32-c3-idf.yaml
@@ -0,0 +1,6 @@
+i2c:
+  - id: i2c_lps22
+    scl: 5
+    sda: 4
+
+<<: !include common.yaml
diff --git a/tests/components/lps22/test.esp32-idf.yaml b/tests/components/lps22/test.esp32-idf.yaml
new file mode 100644
index 0000000000..0da6a9577e
--- /dev/null
+++ b/tests/components/lps22/test.esp32-idf.yaml
@@ -0,0 +1,6 @@
+i2c:
+  - id: i2c_lps22
+    scl: 16
+    sda: 17
+
+<<: !include common.yaml
diff --git a/tests/components/lps22/test.esp8266-ard.yaml b/tests/components/lps22/test.esp8266-ard.yaml
new file mode 100644
index 0000000000..6091393d31
--- /dev/null
+++ b/tests/components/lps22/test.esp8266-ard.yaml
@@ -0,0 +1,6 @@
+i2c:
+  - id: i2c_lps22
+    scl: 5
+    sda: 4
+
+<<: !include common.yaml
diff --git a/tests/components/lps22/test.rp2040-ard.yaml b/tests/components/lps22/test.rp2040-ard.yaml
new file mode 100644
index 0000000000..6091393d31
--- /dev/null
+++ b/tests/components/lps22/test.rp2040-ard.yaml
@@ -0,0 +1,6 @@
+i2c:
+  - id: i2c_lps22
+    scl: 5
+    sda: 4
+
+<<: !include common.yaml

From 78eb236a4ad8b5ce70a7f6eacfa54e1d8909000b Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Wed, 9 Jul 2025 09:47:42 +1200
Subject: [PATCH 286/293] [nfc] Update code to use ``format_hex_pretty``
 (#9384)

---
 esphome/components/nfc/nfc.cpp | 25 +++----------------------
 esphome/components/nfc/nfc.h   |  6 +++---
 2 files changed, 6 insertions(+), 25 deletions(-)

diff --git a/esphome/components/nfc/nfc.cpp b/esphome/components/nfc/nfc.cpp
index cf5a7f5ef1..d3a2481693 100644
--- a/esphome/components/nfc/nfc.cpp
+++ b/esphome/components/nfc/nfc.cpp
@@ -1,5 +1,6 @@
 #include "nfc.h"
 #include 
+#include "esphome/core/helpers.h"
 #include "esphome/core/log.h"
 
 namespace esphome {
@@ -7,29 +8,9 @@ namespace nfc {
 
 static const char *const TAG = "nfc";
 
-std::string format_uid(std::vector &uid) {
-  char buf[(uid.size() * 2) + uid.size() - 1];
-  int offset = 0;
-  for (size_t i = 0; i < uid.size(); i++) {
-    const char *format = "%02X";
-    if (i + 1 < uid.size())
-      format = "%02X-";
-    offset += sprintf(buf + offset, format, uid[i]);
-  }
-  return std::string(buf);
-}
+std::string format_uid(const std::vector &uid) { return format_hex_pretty(uid, '-', false); }
 
-std::string format_bytes(std::vector &bytes) {
-  char buf[(bytes.size() * 2) + bytes.size() - 1];
-  int offset = 0;
-  for (size_t i = 0; i < bytes.size(); i++) {
-    const char *format = "%02X";
-    if (i + 1 < bytes.size())
-      format = "%02X ";
-    offset += sprintf(buf + offset, format, bytes[i]);
-  }
-  return std::string(buf);
-}
+std::string format_bytes(const std::vector &bytes) { return format_hex_pretty(bytes, ' ', false); }
 
 uint8_t guess_tag_type(uint8_t uid_length) {
   if (uid_length == 4) {
diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h
index 2e5c5cd9c5..9879cfdb03 100644
--- a/esphome/components/nfc/nfc.h
+++ b/esphome/components/nfc/nfc.h
@@ -2,8 +2,8 @@
 
 #include "esphome/core/helpers.h"
 #include "esphome/core/log.h"
-#include "ndef_record.h"
 #include "ndef_message.h"
+#include "ndef_record.h"
 #include "nfc_tag.h"
 
 #include 
@@ -53,8 +53,8 @@ static const uint8_t DEFAULT_KEY[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
 static const uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7};
 static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5};
 
-std::string format_uid(std::vector &uid);
-std::string format_bytes(std::vector &bytes);
+std::string format_uid(const std::vector &uid);
+std::string format_bytes(const std::vector &bytes);
 
 uint8_t guess_tag_type(uint8_t uid_length);
 uint8_t get_mifare_classic_ndef_start_index(std::vector &data);

From 05c53644900093d5bf9110d1d930444a8f3f694b Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Wed, 9 Jul 2025 10:13:21 +1200
Subject: [PATCH 287/293] [helpers] Fix ``format_hex_pretty`` resize without
 separator (#9389)

Co-authored-by: RubenKelevra 
---
 esphome/core/helpers.cpp | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp
index 0c11c5d486..b46077af02 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -263,7 +263,7 @@ std::string format_hex_pretty(const uint8_t *data, size_t length, char separator
     return "";
   std::string ret;
   uint8_t multiple = separator ? 3 : 2;  // 3 if separator is not \0, 2 otherwise
-  ret.resize(multiple * length - 1);
+  ret.resize(multiple * length - (separator ? 1 : 0));
   for (size_t i = 0; i < length; i++) {
     ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
     ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
@@ -283,7 +283,7 @@ std::string format_hex_pretty(const uint16_t *data, size_t length, char separato
     return "";
   std::string ret;
   uint8_t multiple = separator ? 5 : 4;  // 5 if separator is not \0, 4 otherwise
-  ret.resize(multiple * length - 1);
+  ret.resize(multiple * length - (separator ? 1 : 0));
   for (size_t i = 0; i < length; i++) {
     ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF000) >> 12);
     ret[multiple * i + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8);
@@ -304,7 +304,7 @@ std::string format_hex_pretty(const std::string &data, char separator, bool show
     return "";
   std::string ret;
   uint8_t multiple = separator ? 3 : 2;  // 3 if separator is not \0, 2 otherwise
-  ret.resize(multiple * data.length() - 1);
+  ret.resize(multiple * data.length() - (separator ? 1 : 0));
   for (size_t i = 0; i < data.length(); i++) {
     ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
     ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);

From 4158a5c2a3af1208b09dcd092096a807eab8c0e3 Mon Sep 17 00:00:00 2001
From: Petr Kejval 
Date: Wed, 9 Jul 2025 00:50:45 +0200
Subject: [PATCH 288/293] Add support for GL-R01 I2C - Time of Flight sensor
 (#8329)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 CODEOWNERS                                    |  1 +
 esphome/components/gl_r01_i2c/__init__.py     |  0
 esphome/components/gl_r01_i2c/gl_r01_i2c.cpp  | 68 +++++++++++++++++++
 esphome/components/gl_r01_i2c/gl_r01_i2c.h    | 22 ++++++
 esphome/components/gl_r01_i2c/sensor.py       | 36 ++++++++++
 tests/components/gl_r01_i2c/common.yaml       | 12 ++++
 .../components/gl_r01_i2c/test.esp32-ard.yaml |  5 ++
 .../gl_r01_i2c/test.esp32-c3-ard.yaml         |  5 ++
 .../gl_r01_i2c/test.esp32-c3-idf.yaml         |  5 ++
 .../components/gl_r01_i2c/test.esp32-idf.yaml |  5 ++
 .../gl_r01_i2c/test.esp8266-ard.yaml          |  5 ++
 .../gl_r01_i2c/test.rp2040-ard.yaml           |  5 ++
 12 files changed, 169 insertions(+)
 create mode 100644 esphome/components/gl_r01_i2c/__init__.py
 create mode 100644 esphome/components/gl_r01_i2c/gl_r01_i2c.cpp
 create mode 100644 esphome/components/gl_r01_i2c/gl_r01_i2c.h
 create mode 100644 esphome/components/gl_r01_i2c/sensor.py
 create mode 100644 tests/components/gl_r01_i2c/common.yaml
 create mode 100644 tests/components/gl_r01_i2c/test.esp32-ard.yaml
 create mode 100644 tests/components/gl_r01_i2c/test.esp32-c3-ard.yaml
 create mode 100644 tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml
 create mode 100644 tests/components/gl_r01_i2c/test.esp32-idf.yaml
 create mode 100644 tests/components/gl_r01_i2c/test.esp8266-ard.yaml
 create mode 100644 tests/components/gl_r01_i2c/test.rp2040-ard.yaml

diff --git a/CODEOWNERS b/CODEOWNERS
index faece3fdd7..9b4681fcf2 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -170,6 +170,7 @@ esphome/components/ft5x06/* @clydebarrow
 esphome/components/ft63x6/* @gpambrozio
 esphome/components/gcja5/* @gcormier
 esphome/components/gdk101/* @Szewcson
+esphome/components/gl_r01_i2c/* @pkejval
 esphome/components/globals/* @esphome/core
 esphome/components/gp2y1010au0f/* @zry98
 esphome/components/gp8403/* @jesserockz
diff --git a/esphome/components/gl_r01_i2c/__init__.py b/esphome/components/gl_r01_i2c/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp b/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp
new file mode 100644
index 0000000000..5a24c63525
--- /dev/null
+++ b/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp
@@ -0,0 +1,68 @@
+#include "esphome/core/log.h"
+#include "esphome/core/hal.h"
+#include "gl_r01_i2c.h"
+
+namespace esphome {
+namespace gl_r01_i2c {
+
+static const char *const TAG = "gl_r01_i2c";
+
+// Register definitions from datasheet
+static const uint8_t REG_VERSION = 0x00;
+static const uint8_t REG_DISTANCE = 0x02;
+static const uint8_t REG_TRIGGER = 0x10;
+static const uint8_t CMD_TRIGGER = 0xB0;
+static const uint8_t RESTART_CMD1 = 0x5A;
+static const uint8_t RESTART_CMD2 = 0xA5;
+static const uint8_t READ_DELAY = 40;  // minimum milliseconds from datasheet to safely read measurement result
+
+void GLR01I2CComponent::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up GL-R01 I2C...");
+  // Verify sensor presence
+  if (!this->read_byte_16(REG_VERSION, &this->version_)) {
+    ESP_LOGE(TAG, "Failed to communicate with GL-R01 I2C sensor!");
+    this->mark_failed();
+    return;
+  }
+  ESP_LOGD(TAG, "Found GL-R01 I2C with version 0x%04X", this->version_);
+}
+
+void GLR01I2CComponent::dump_config() {
+  ESP_LOGCONFIG(TAG, "GL-R01 I2C:");
+  ESP_LOGCONFIG(TAG, " Firmware Version: 0x%04X", this->version_);
+  LOG_I2C_DEVICE(this);
+  LOG_SENSOR(" ", "Distance", this);
+}
+
+void GLR01I2CComponent::update() {
+  // Trigger a new measurement
+  if (!this->write_byte(REG_TRIGGER, CMD_TRIGGER)) {
+    ESP_LOGE(TAG, "Failed to trigger measurement!");
+    this->status_set_warning();
+    return;
+  }
+
+  // Schedule reading the result after the read delay
+  this->set_timeout(READ_DELAY, [this]() { this->read_distance_(); });
+}
+
+void GLR01I2CComponent::read_distance_() {
+  uint16_t distance = 0;
+  if (!this->read_byte_16(REG_DISTANCE, &distance)) {
+    ESP_LOGE(TAG, "Failed to read distance value!");
+    this->status_set_warning();
+    return;
+  }
+
+  if (distance == 0xFFFF) {
+    ESP_LOGW(TAG, "Invalid measurement received!");
+    this->status_set_warning();
+  } else {
+    ESP_LOGV(TAG, "Distance: %umm", distance);
+    this->publish_state(distance);
+    this->status_clear_warning();
+  }
+}
+
+}  // namespace gl_r01_i2c
+}  // namespace esphome
diff --git a/esphome/components/gl_r01_i2c/gl_r01_i2c.h b/esphome/components/gl_r01_i2c/gl_r01_i2c.h
new file mode 100644
index 0000000000..9a7aa023fd
--- /dev/null
+++ b/esphome/components/gl_r01_i2c/gl_r01_i2c.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/i2c/i2c.h"
+
+namespace esphome {
+namespace gl_r01_i2c {
+
+class GLR01I2CComponent : public sensor::Sensor, public i2c::I2CDevice, public PollingComponent {
+ public:
+  void setup() override;
+  void dump_config() override;
+  void update() override;
+
+ protected:
+  void read_distance_();
+  uint16_t version_{0};
+};
+
+}  // namespace gl_r01_i2c
+}  // namespace esphome
diff --git a/esphome/components/gl_r01_i2c/sensor.py b/esphome/components/gl_r01_i2c/sensor.py
new file mode 100644
index 0000000000..9f6f75faf7
--- /dev/null
+++ b/esphome/components/gl_r01_i2c/sensor.py
@@ -0,0 +1,36 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import i2c, sensor
+from esphome.const import (
+    CONF_ID,
+    DEVICE_CLASS_DISTANCE,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_MILLIMETER,
+)
+
+CODEOWNERS = ["@pkejval"]
+DEPENDENCIES = ["i2c"]
+
+gl_r01_i2c_ns = cg.esphome_ns.namespace("gl_r01_i2c")
+GLR01I2CComponent = gl_r01_i2c_ns.class_(
+    "GLR01I2CComponent", i2c.I2CDevice, cg.PollingComponent
+)
+
+CONFIG_SCHEMA = (
+    sensor.sensor_schema(
+        GLR01I2CComponent,
+        unit_of_measurement=UNIT_MILLIMETER,
+        accuracy_decimals=0,
+        device_class=DEVICE_CLASS_DISTANCE,
+        state_class=STATE_CLASS_MEASUREMENT,
+    )
+    .extend(cv.polling_component_schema("60s"))
+    .extend(i2c.i2c_device_schema(0x74))
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
+    await i2c.register_i2c_device(var, config)
diff --git a/tests/components/gl_r01_i2c/common.yaml b/tests/components/gl_r01_i2c/common.yaml
new file mode 100644
index 0000000000..fe0705bdc6
--- /dev/null
+++ b/tests/components/gl_r01_i2c/common.yaml
@@ -0,0 +1,12 @@
+i2c:
+  - id: i2c_gl_r01_i2c
+    scl: ${scl_pin}
+    sda: ${sda_pin}
+
+sensor:
+  - platform: gl_r01_i2c
+    id: tof
+    name: "ToF sensor"
+    i2c_id: i2c_gl_r01_i2c
+    address: 0x74
+    update_interval: 15s
diff --git a/tests/components/gl_r01_i2c/test.esp32-ard.yaml b/tests/components/gl_r01_i2c/test.esp32-ard.yaml
new file mode 100644
index 0000000000..63c3bd6afd
--- /dev/null
+++ b/tests/components/gl_r01_i2c/test.esp32-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO16
+  sda_pin: GPIO17
+
+<<: !include common.yaml
diff --git a/tests/components/gl_r01_i2c/test.esp32-c3-ard.yaml b/tests/components/gl_r01_i2c/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/gl_r01_i2c/test.esp32-c3-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml b/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/gl_r01_i2c/test.esp32-idf.yaml b/tests/components/gl_r01_i2c/test.esp32-idf.yaml
new file mode 100644
index 0000000000..63c3bd6afd
--- /dev/null
+++ b/tests/components/gl_r01_i2c/test.esp32-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO16
+  sda_pin: GPIO17
+
+<<: !include common.yaml
diff --git a/tests/components/gl_r01_i2c/test.esp8266-ard.yaml b/tests/components/gl_r01_i2c/test.esp8266-ard.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/gl_r01_i2c/test.esp8266-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/gl_r01_i2c/test.rp2040-ard.yaml b/tests/components/gl_r01_i2c/test.rp2040-ard.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/gl_r01_i2c/test.rp2040-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml

From 5c8b330eaa5c6c28fdc3cc84ae65ff8f83151a88 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Wed, 9 Jul 2025 10:51:17 +1200
Subject: [PATCH 289/293] [esp32] Improve flexibility of ``only_on_variant``
 (#9390)

---
 esphome/components/esp32/__init__.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py
index b4c7a4e05b..8408f902ef 100644
--- a/esphome/components/esp32/__init__.py
+++ b/esphome/components/esp32/__init__.py
@@ -189,7 +189,7 @@ def get_download_types(storage_json):
     ]
 
 
-def only_on_variant(*, supported=None, unsupported=None):
+def only_on_variant(*, supported=None, unsupported=None, msg_prefix="This feature"):
     """Config validator for features only available on some ESP32 variants."""
     if supported is not None and not isinstance(supported, list):
         supported = [supported]
@@ -200,11 +200,11 @@ def only_on_variant(*, supported=None, unsupported=None):
         variant = get_esp32_variant()
         if supported is not None and variant not in supported:
             raise cv.Invalid(
-                f"This feature is only available on {', '.join(supported)}"
+                f"{msg_prefix} is only available on {', '.join(supported)}"
             )
         if unsupported is not None and variant in unsupported:
             raise cv.Invalid(
-                f"This feature is not available on {', '.join(unsupported)}"
+                f"{msg_prefix} is not available on {', '.join(unsupported)}"
             )
         return obj
 

From 86c6e4da2a516b2c89580d279d919536c65d9f2c Mon Sep 17 00:00:00 2001
From: Craig Andrews 
Date: Tue, 8 Jul 2025 19:30:06 -0400
Subject: [PATCH 290/293] ESP_EXT1_WAKEUP_ANY_LOW is for s2/s3/c6/h2;
 ESP_EXT1_WAKEUP_ALL_LOW otherwise (#9387)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/deep_sleep/__init__.py | 39 +++++++++++++++++------
 1 file changed, 30 insertions(+), 9 deletions(-)

diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py
index 55826f52bb..05ae60239d 100644
--- a/esphome/components/deep_sleep/__init__.py
+++ b/esphome/components/deep_sleep/__init__.py
@@ -1,6 +1,6 @@
 from esphome import automation, pins
 import esphome.codegen as cg
-from esphome.components import time
+from esphome.components import esp32, time
 from esphome.components.esp32 import get_esp32_variant
 from esphome.components.esp32.const import (
     VARIANT_ESP32,
@@ -116,12 +116,20 @@ def validate_pin_number(value):
     return value
 
 
-def validate_config(config):
-    if get_esp32_variant() == VARIANT_ESP32C3 and CONF_ESP32_EXT1_WAKEUP in config:
-        raise cv.Invalid("ESP32-C3 does not support wakeup from touch.")
-    if get_esp32_variant() == VARIANT_ESP32C3 and CONF_TOUCH_WAKEUP in config:
-        raise cv.Invalid("ESP32-C3 does not support wakeup from ext1")
-    return config
+def _validate_ex1_wakeup_mode(value):
+    if value == "ALL_LOW":
+        esp32.only_on_variant(supported=[VARIANT_ESP32], msg_prefix="ALL_LOW")(value)
+    if value == "ANY_LOW":
+        esp32.only_on_variant(
+            supported=[
+                VARIANT_ESP32S2,
+                VARIANT_ESP32S3,
+                VARIANT_ESP32C6,
+                VARIANT_ESP32H2,
+            ],
+            msg_prefix="ANY_LOW",
+        )(value)
+    return value
 
 
 deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep")
@@ -148,6 +156,7 @@ WAKEUP_PIN_MODES = {
 esp_sleep_ext1_wakeup_mode_t = cg.global_ns.enum("esp_sleep_ext1_wakeup_mode_t")
 Ext1Wakeup = deep_sleep_ns.struct("Ext1Wakeup")
 EXT1_WAKEUP_MODES = {
+    "ANY_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_LOW,
     "ALL_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ALL_LOW,
     "ANY_HIGH": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_HIGH,
 }
@@ -187,16 +196,28 @@ CONFIG_SCHEMA = cv.All(
             ),
             cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All(
                 cv.only_on_esp32,
+                esp32.only_on_variant(
+                    unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from ext1"
+                ),
                 cv.Schema(
                     {
                         cv.Required(CONF_PINS): cv.ensure_list(
                             pins.internal_gpio_input_pin_schema, validate_pin_number
                         ),
-                        cv.Required(CONF_MODE): cv.enum(EXT1_WAKEUP_MODES, upper=True),
+                        cv.Required(CONF_MODE): cv.All(
+                            cv.enum(EXT1_WAKEUP_MODES, upper=True),
+                            _validate_ex1_wakeup_mode,
+                        ),
                     }
                 ),
             ),
-            cv.Optional(CONF_TOUCH_WAKEUP): cv.All(cv.only_on_esp32, cv.boolean),
+            cv.Optional(CONF_TOUCH_WAKEUP): cv.All(
+                cv.only_on_esp32,
+                esp32.only_on_variant(
+                    unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from touch"
+                ),
+                cv.boolean,
+            ),
         }
     ).extend(cv.COMPONENT_SCHEMA),
     cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]),

From 0ccc5e340e6089bc44f9149818bc3de475d2fbd9 Mon Sep 17 00:00:00 2001
From: Merikei <73759842+Merikei@users.noreply.github.com>
Date: Tue, 8 Jul 2025 23:52:30 +0000
Subject: [PATCH 291/293] [apds9960] Add 0x9E ID (#9392)

---
 esphome/components/apds9960/apds9960.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/apds9960/apds9960.cpp b/esphome/components/apds9960/apds9960.cpp
index 4a6ce371e5..b736e6b8b0 100644
--- a/esphome/components/apds9960/apds9960.cpp
+++ b/esphome/components/apds9960/apds9960.cpp
@@ -23,7 +23,7 @@ void APDS9960::setup() {
     return;
   }
 
-  if (id != 0xAB && id != 0x9C && id != 0xA8) {  // APDS9960 all should have one of these IDs
+  if (id != 0xAB && id != 0x9C && id != 0xA8 && id != 0x9E) {  // APDS9960 all should have one of these IDs
     this->error_code_ = WRONG_ID;
     this->mark_failed();
     return;

From 5235c80781b80e90103e8c8e3f31a2e3cee7d2dd Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 8 Jul 2025 23:54:33 +0000
Subject: [PATCH 292/293] Bump aioesphomeapi from 34.1.0 to 34.2.0 (#9391)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index a6bcebaeea..d056f22e28 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,7 +13,7 @@ platformio==6.1.18  # When updating platformio, also update /docker/Dockerfile
 esptool==4.9.0
 click==8.1.7
 esphome-dashboard==20250514.0
-aioesphomeapi==34.1.0
+aioesphomeapi==34.2.0
 zeroconf==0.147.0
 puremagic==1.30
 ruamel.yaml==0.18.14 # dashboard_import

From 267574f24c4ed6104c91e4a1eedef3e4eabea0ca Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Wed, 9 Jul 2025 12:06:52 +1200
Subject: [PATCH 293/293] Bump version to 2025.7.0b1

---
 Doxyfile         | 2 +-
 esphome/const.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Doxyfile b/Doxyfile
index 03d432b924..3d6147135b 100644
--- a/Doxyfile
+++ b/Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME           = ESPHome
 # could be handy for archiving the generated documentation or if some version
 # control system is used.
 
-PROJECT_NUMBER         = 2025.7.0-dev
+PROJECT_NUMBER         = 2025.7.0b1
 
 # Using the PROJECT_BRIEF tag one can provide an optional one line description
 # for a project that appears at the top of each page and should give viewer a
diff --git a/esphome/const.py b/esphome/const.py
index 085b9b39b8..94d1379b37 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -4,7 +4,7 @@ from enum import Enum
 
 from esphome.enum import StrEnum
 
-__version__ = "2025.7.0-dev"
+__version__ = "2025.7.0b1"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 VALID_SUBSTITUTIONS_CHARACTERS = (