From fd3c05b42e8097487fa22daf797e9992d35e7160 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Wed, 1 Oct 2025 03:33:56 +0200 Subject: [PATCH 01/99] [substitutions] fix #10825 set evaluation error (#10830) --- esphome/components/substitutions/jinja.py | 16 +++++++++++++--- .../substitutions/00-simple_var.approved.yaml | 7 +++++++ .../substitutions/00-simple_var.input.yaml | 7 +++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/esphome/components/substitutions/jinja.py b/esphome/components/substitutions/jinja.py index c6e40a668d..e7164d8fff 100644 --- a/esphome/components/substitutions/jinja.py +++ b/esphome/components/substitutions/jinja.py @@ -1,9 +1,10 @@ +from ast import literal_eval import logging import math import re import jinja2 as jinja -from jinja2.nativetypes import NativeEnvironment +from jinja2.sandbox import SandboxedEnvironment TemplateError = jinja.TemplateError TemplateSyntaxError = jinja.TemplateSyntaxError @@ -70,7 +71,7 @@ class Jinja: """ def __init__(self, context_vars): - self.env = NativeEnvironment( + self.env = SandboxedEnvironment( trim_blocks=True, lstrip_blocks=True, block_start_string="<%", @@ -90,6 +91,15 @@ class Jinja: **SAFE_GLOBAL_FUNCTIONS, } + def safe_eval(self, expr): + try: + result = literal_eval(expr) + if not isinstance(result, str): + return result + except (ValueError, SyntaxError, MemoryError, TypeError): + pass + return expr + def expand(self, content_str): """ Renders a string that may contain Jinja expressions or statements @@ -106,7 +116,7 @@ class Jinja: override_vars = content_str.upvalues try: template = self.env.from_string(content_str) - result = template.render(override_vars) + result = self.safe_eval(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". diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml index c59975b2ae..795a788f62 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml @@ -5,6 +5,9 @@ substitutions: var21: '79' value: 33 values: 44 + position: + x: 79 + y: 82 esphome: name: test @@ -26,3 +29,7 @@ test_list: - Literal $values ${are not substituted} - ["list $value", "${is not}", "${substituted}"] - {"$dictionary": "$value", "${is not}": "${substituted}"} + - |- + {{{ "x", "79"}, { "y", "82"}}} + - '{{{"AA"}}}' + - '"HELLO"' diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml index 3b7e7a6b4e..722e116d36 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml @@ -8,6 +8,9 @@ substitutions: var21: "79" value: 33 values: 44 + position: + x: 79 + y: 82 test_list: - "$var1" @@ -27,3 +30,7 @@ test_list: - !literal Literal $values ${are not substituted} - !literal ["list $value", "${is not}", "${substituted}"] - !literal {"$dictionary": "$value", "${is not}": "${substituted}"} + - |- # Test parsing things that look like a python set of sets when rendered: + {{{ "x", "${ position.x }"}, { "y", "${ position.y }"}}} + - ${ '{{{"AA"}}}' } + - ${ '"HELLO"' } From 922f4b6352cb2d41ea9240785b050eca6c7c8fef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Oct 2025 04:52:35 +0200 Subject: [PATCH 02/99] [web_server] Optimize handler methods with lookup tables to reduce flash usage (#10951) --- esphome/components/web_server/web_server.cpp | 204 +++++++++++-------- 1 file changed, 118 insertions(+), 86 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 03bc17f4fa..33141c2049 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -829,15 +829,28 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa } auto call = obj->make_call(); - if (match.method_equals("open")) { - call.set_command_open(); - } else if (match.method_equals("close")) { - call.set_command_close(); - } else if (match.method_equals("stop")) { - call.set_command_stop(); - } else if (match.method_equals("toggle")) { - call.set_command_toggle(); - } else if (!match.method_equals("set")) { + + // Lookup table for cover methods + static const struct { + const char *name; + cover::CoverCall &(cover::CoverCall::*action)(); + } METHODS[] = { + {"open", &cover::CoverCall::set_command_open}, + {"close", &cover::CoverCall::set_command_close}, + {"stop", &cover::CoverCall::set_command_stop}, + {"toggle", &cover::CoverCall::set_command_toggle}, + }; + + bool found = false; + for (const auto &method : METHODS) { + if (match.method_equals(method.name)) { + (call.*method.action)(); + found = true; + break; + } + } + + if (!found && !match.method_equals("set")) { request->send(404); return; } @@ -1483,15 +1496,28 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa } auto call = obj->make_call(); - if (match.method_equals("open")) { - call.set_command_open(); - } else if (match.method_equals("close")) { - call.set_command_close(); - } else if (match.method_equals("stop")) { - call.set_command_stop(); - } else if (match.method_equals("toggle")) { - call.set_command_toggle(); - } else if (!match.method_equals("set")) { + + // Lookup table for valve methods + static const struct { + const char *name; + valve::ValveCall &(valve::ValveCall::*action)(); + } METHODS[] = { + {"open", &valve::ValveCall::set_command_open}, + {"close", &valve::ValveCall::set_command_close}, + {"stop", &valve::ValveCall::set_command_stop}, + {"toggle", &valve::ValveCall::set_command_toggle}, + }; + + bool found = false; + for (const auto &method : METHODS) { + if (match.method_equals(method.name)) { + (call.*method.action)(); + found = true; + break; + } + } + + if (!found && !match.method_equals("set")) { request->send(404); return; } @@ -1555,17 +1581,28 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques auto call = obj->make_call(); parse_string_param_(request, "code", call, &decltype(call)::set_code); - if (match.method_equals("disarm")) { - call.disarm(); - } else if (match.method_equals("arm_away")) { - call.arm_away(); - } else if (match.method_equals("arm_home")) { - call.arm_home(); - } else if (match.method_equals("arm_night")) { - call.arm_night(); - } else if (match.method_equals("arm_vacation")) { - call.arm_vacation(); - } else { + // Lookup table for alarm control panel methods + static const struct { + const char *name; + alarm_control_panel::AlarmControlPanelCall &(alarm_control_panel::AlarmControlPanelCall::*action)(); + } METHODS[] = { + {"disarm", &alarm_control_panel::AlarmControlPanelCall::disarm}, + {"arm_away", &alarm_control_panel::AlarmControlPanelCall::arm_away}, + {"arm_home", &alarm_control_panel::AlarmControlPanelCall::arm_home}, + {"arm_night", &alarm_control_panel::AlarmControlPanelCall::arm_night}, + {"arm_vacation", &alarm_control_panel::AlarmControlPanelCall::arm_vacation}, + }; + + bool found = false; + for (const auto &method : METHODS) { + if (match.method_equals(method.name)) { + (call.*method.action)(); + found = true; + break; + } + } + + if (!found) { request->send(404); return; } @@ -1731,24 +1768,24 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const { const auto &url = request->url(); const auto method = request->method(); - // Simple URL checks - if (url == "/") - return true; - + // Static URL checks + static const char *const STATIC_URLS[] = { + "/", #ifdef USE_ARDUINO - if (url == "/events") - return true; + "/events", #endif - #ifdef USE_WEBSERVER_CSS_INCLUDE - if (url == "/0.css") - return true; + "/0.css", #endif - #ifdef USE_WEBSERVER_JS_INCLUDE - if (url == "/0.js") - return true; + "/0.js", #endif + }; + + for (const auto &static_url : STATIC_URLS) { + if (url == static_url) + return true; + } #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS if (method == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) @@ -1768,92 +1805,87 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const { if (!is_get_or_post) return false; - // GET-only components - if (is_get) { + // Use lookup tables for domain checks + static const char *const GET_ONLY_DOMAINS[] = { #ifdef USE_SENSOR - if (match.domain_equals("sensor")) - return true; + "sensor", #endif #ifdef USE_BINARY_SENSOR - if (match.domain_equals("binary_sensor")) - return true; + "binary_sensor", #endif #ifdef USE_TEXT_SENSOR - if (match.domain_equals("text_sensor")) - return true; + "text_sensor", #endif #ifdef USE_EVENT - if (match.domain_equals("event")) - return true; + "event", #endif - } + }; - // GET+POST components - if (is_get_or_post) { + static const char *const GET_POST_DOMAINS[] = { #ifdef USE_SWITCH - if (match.domain_equals("switch")) - return true; + "switch", #endif #ifdef USE_BUTTON - if (match.domain_equals("button")) - return true; + "button", #endif #ifdef USE_FAN - if (match.domain_equals("fan")) - return true; + "fan", #endif #ifdef USE_LIGHT - if (match.domain_equals("light")) - return true; + "light", #endif #ifdef USE_COVER - if (match.domain_equals("cover")) - return true; + "cover", #endif #ifdef USE_NUMBER - if (match.domain_equals("number")) - return true; + "number", #endif #ifdef USE_DATETIME_DATE - if (match.domain_equals("date")) - return true; + "date", #endif #ifdef USE_DATETIME_TIME - if (match.domain_equals("time")) - return true; + "time", #endif #ifdef USE_DATETIME_DATETIME - if (match.domain_equals("datetime")) - return true; + "datetime", #endif #ifdef USE_TEXT - if (match.domain_equals("text")) - return true; + "text", #endif #ifdef USE_SELECT - if (match.domain_equals("select")) - return true; + "select", #endif #ifdef USE_CLIMATE - if (match.domain_equals("climate")) - return true; + "climate", #endif #ifdef USE_LOCK - if (match.domain_equals("lock")) - return true; + "lock", #endif #ifdef USE_VALVE - if (match.domain_equals("valve")) - return true; + "valve", #endif #ifdef USE_ALARM_CONTROL_PANEL - if (match.domain_equals("alarm_control_panel")) - return true; + "alarm_control_panel", #endif #ifdef USE_UPDATE - if (match.domain_equals("update")) - return true; + "update", #endif + }; + + // Check GET-only domains + if (is_get) { + for (const auto &domain : GET_ONLY_DOMAINS) { + if (match.domain_equals(domain)) + return true; + } + } + + // Check GET+POST domains + if (is_get_or_post) { + for (const auto &domain : GET_POST_DOMAINS) { + if (match.domain_equals(domain)) + return true; + } } return false; From 848ba6b717979fd0ca54807116d366056f365735 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 1 Oct 2025 23:29:10 +1300 Subject: [PATCH 03/99] [psram] Fix invalid variant error, add `supported()` check (#10962) --- esphome/components/psram/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py index d559b2436b..6b85e7f720 100644 --- a/esphome/components/psram/__init__.py +++ b/esphome/components/psram/__init__.py @@ -62,6 +62,11 @@ SPIRAM_SPEEDS = { } +def supported() -> bool: + variant = get_esp32_variant() + return variant in SPIRAM_MODES + + def validate_psram_mode(config): esp32_config = fv.full_config.get()[PLATFORM_ESP32] if config[CONF_SPEED] == "120MHZ": @@ -95,7 +100,7 @@ def get_config_schema(config): variant = get_esp32_variant() speeds = [f"{s}MHZ" for s in SPIRAM_SPEEDS.get(variant, [])] if not speeds: - return cv.Invalid("PSRAM is not supported on this chip") + raise cv.Invalid("PSRAM is not supported on this chip") modes = SPIRAM_MODES[variant] return cv.Schema( { From c95180504a178072903d61bc4cf8e5c632b546b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Sep 2025 22:42:58 -0500 Subject: [PATCH 04/99] [api] Prevent API from overriding noise encryption keys set in YAML (#10927) --- esphome/components/api/__init__.py | 1 + esphome/components/api/api_server.cpp | 11 +++- .../noise_encryption_key_protection.yaml | 10 ++++ .../test_noise_encryption_key_protection.py | 51 +++++++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 tests/integration/fixtures/noise_encryption_key_protection.yaml create mode 100644 tests/integration/test_noise_encryption_key_protection.py diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 5fb84d3c21..b120503a2e 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -193,6 +193,7 @@ async def to_code(config): if key := encryption_config.get(CONF_KEY): decoded = base64.b64decode(key) cg.add(var.set_noise_psk(list(decoded))) + cg.add_define("USE_API_NOISE_PSK_FROM_YAML") else: # No key provided, but encryption desired # This will allow a plaintext client to provide a noise key, diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 1f38f4a31a..a12cf13ce2 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -37,12 +37,14 @@ void APIServer::setup() { this->noise_pref_ = global_preferences->make_preference(hash, true); +#ifndef USE_API_NOISE_PSK_FROM_YAML + // Only load saved PSK if not set from YAML SavedNoisePsk noise_pref_saved{}; if (this->noise_pref_.load(&noise_pref_saved)) { ESP_LOGD(TAG, "Loaded saved Noise PSK"); - this->set_noise_psk(noise_pref_saved.psk); } +#endif #endif // Schedule reboot if no clients connect within timeout @@ -409,6 +411,12 @@ void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeo #ifdef USE_API_NOISE bool APIServer::save_noise_psk(psk_t psk, bool make_active) { +#ifdef USE_API_NOISE_PSK_FROM_YAML + // When PSK is set from YAML, this function should never be called + // but if it is, reject the change + ESP_LOGW(TAG, "Key set in YAML"); + return false; +#else auto &old_psk = this->noise_ctx_->get_psk(); if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) { ESP_LOGW(TAG, "New PSK matches old"); @@ -437,6 +445,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { }); } return true; +#endif } #endif diff --git a/tests/integration/fixtures/noise_encryption_key_protection.yaml b/tests/integration/fixtures/noise_encryption_key_protection.yaml new file mode 100644 index 0000000000..3ce84cd373 --- /dev/null +++ b/tests/integration/fixtures/noise_encryption_key_protection.yaml @@ -0,0 +1,10 @@ +esphome: + name: noise-key-test + +host: + +api: + encryption: + key: "zX9/JHxMKwpP0jUGsF0iESCm1wRvNgR6NkKVOhn7kSs=" + +logger: diff --git a/tests/integration/test_noise_encryption_key_protection.py b/tests/integration/test_noise_encryption_key_protection.py new file mode 100644 index 0000000000..03c43ca8d3 --- /dev/null +++ b/tests/integration/test_noise_encryption_key_protection.py @@ -0,0 +1,51 @@ +"""Integration test for noise encryption key protection from YAML.""" + +from __future__ import annotations + +import base64 + +from aioesphomeapi import InvalidEncryptionKeyAPIError +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_noise_encryption_key_protection( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that noise encryption key set in YAML cannot be changed via API.""" + # The key that's set in the YAML fixture + noise_psk = "zX9/JHxMKwpP0jUGsF0iESCm1wRvNgR6NkKVOhn7kSs=" + + # Keep ESPHome process running throughout all tests + async with run_compiled(yaml_config): + # First connection - test key change attempt + async with api_client_connected(noise_psk=noise_psk) as client: + # Verify connection is established + device_info = await client.device_info() + assert device_info is not None + + # Try to set a new encryption key via API + new_key = base64.b64encode( + b"x" * 32 + ) # Valid 32-byte key in base64 as bytes + + # This should fail since key was set in YAML + success = await client.noise_encryption_set_key(new_key) + assert success is False + + # Reconnect with the original key to verify it still works + async with api_client_connected(noise_psk=noise_psk) as client: + # Verify connection is still successful with original key + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "noise-key-test" + + # Verify that connecting with a wrong key fails + wrong_key = base64.b64encode(b"y" * 32).decode() # Different key + with pytest.raises(InvalidEncryptionKeyAPIError): + async with api_client_connected(noise_psk=wrong_key) as client: + await client.device_info() From 158a59aa83b218e7cc593b706f1332d8f8dc8953 Mon Sep 17 00:00:00 2001 From: Vladimir Makeev Date: Mon, 29 Sep 2025 17:08:51 +0400 Subject: [PATCH 05/99] [sim800l] Fixed ignoring incoming calls. (#10865) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/sim800l/sim800l.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index d97b0ae364..55cadcf182 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -288,11 +288,15 @@ void Sim800LComponent::parse_cmd_(std::string message) { if (item == 3) { // stat uint8_t current_call_state = parse_number(message.substr(start, end - start)).value_or(6); if (current_call_state != this->call_state_) { - ESP_LOGD(TAG, "Call state is now: %d", current_call_state); - if (current_call_state == 0) - this->call_connected_callback_.call(); + if (current_call_state == 4) { + ESP_LOGV(TAG, "Premature call state '4'. Ignoring, waiting for RING"); + } else { + this->call_state_ = current_call_state; + ESP_LOGD(TAG, "Call state is now: %d", current_call_state); + if (current_call_state == 0) + this->call_connected_callback_.call(); + } } - this->call_state_ = current_call_state; break; } // item 4 = "" From 29658b79bc69a5e032170bab3d817f751f128485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Tue, 30 Sep 2025 03:29:16 +0100 Subject: [PATCH 06/99] [voice_assistant] Fix wakeword string being reset while referenced (#10945) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/voice_assistant/voice_assistant.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 743c90e700..a0cf1a155b 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -242,7 +242,6 @@ void VoiceAssistant::loop() { msg.flags = flags; msg.audio_settings = audio_settings; msg.set_wake_word_phrase(StringRef(this->wake_word_)); - this->wake_word_ = ""; // Reset media player state tracking #ifdef USE_MEDIA_PLAYER From 59c0ffb98b6b0998fdbca75795af50c46c2d9cbc Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 1 Oct 2025 23:41:42 +1300 Subject: [PATCH 07/99] Bump version to 2025.9.3 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index b0b92dfd63..d14d1a2adb 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.9.2 +PROJECT_NUMBER = 2025.9.3 # 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 dafd49c066..a7e1752a67 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.9.2" +__version__ = "2025.9.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 4194a940ae4919ef37dd6b889d2ec1080c092dcb Mon Sep 17 00:00:00 2001 From: Piotr Szulc Date: Wed, 1 Oct 2025 13:10:37 +0200 Subject: [PATCH 08/99] [remote_transmitter] fix sending codes on libretiny (#10959) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../components/remote_transmitter/remote_transmitter.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/remote_transmitter/remote_transmitter.cpp b/esphome/components/remote_transmitter/remote_transmitter.cpp index 79d9cda93b..347e9d9d33 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter.cpp @@ -40,7 +40,13 @@ void RemoteTransmitterComponent::await_target_time_() { if (this->target_time_ == 0) { this->target_time_ = current_time; } else if ((int32_t) (this->target_time_ - current_time) > 0) { +#if defined(USE_LIBRETINY) + // busy loop for libretiny is required (see the comment inside micros() in wiring.c) + while ((int32_t) (this->target_time_ - micros()) > 0) + ; +#else delayMicroseconds(this->target_time_ - current_time); +#endif } } From 5cef75dbe11e4412c39dd5497d926726206e8be2 Mon Sep 17 00:00:00 2001 From: mrtoy-me <118446898+mrtoy-me@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:22:02 +1000 Subject: [PATCH 09/99] [hdc1080] remove delays and fix no check for sensor nullptr (#10947) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/hdc1080/hdc1080.cpp | 76 ++++++++++++++------------ esphome/components/hdc1080/hdc1080.h | 4 +- 2 files changed, 43 insertions(+), 37 deletions(-) diff --git a/esphome/components/hdc1080/hdc1080.cpp b/esphome/components/hdc1080/hdc1080.cpp index 6d16133c36..71b7cd7e6e 100644 --- a/esphome/components/hdc1080/hdc1080.cpp +++ b/esphome/components/hdc1080/hdc1080.cpp @@ -7,24 +7,20 @@ namespace hdc1080 { static const char *const TAG = "hdc1080"; -static const uint8_t HDC1080_ADDRESS = 0x40; // 0b1000000 from datasheet static const uint8_t HDC1080_CMD_CONFIGURATION = 0x02; static const uint8_t HDC1080_CMD_TEMPERATURE = 0x00; static const uint8_t HDC1080_CMD_HUMIDITY = 0x01; void HDC1080Component::setup() { - const uint8_t data[2] = { - 0b00000000, // resolution 14bit for both humidity and temperature - 0b00000000 // reserved - }; + const uint8_t config[2] = {0x00, 0x00}; // resolution 14bit for both humidity and temperature - if (!this->write_bytes(HDC1080_CMD_CONFIGURATION, data, 2)) { - // as instruction is same as powerup defaults (for now), interpret as warning if this fails - ESP_LOGW(TAG, "HDC1080 initial config instruction error"); - this->status_set_warning(); + // if configuration fails - there is a problem + if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) { + this->mark_failed(); return; } } + void HDC1080Component::dump_config() { ESP_LOGCONFIG(TAG, "HDC1080:"); LOG_I2C_DEVICE(this); @@ -35,39 +31,51 @@ void HDC1080Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); } + void HDC1080Component::update() { - uint16_t raw_temp; + // regardless of what sensor/s are defined in yaml configuration + // the hdc1080 setup configuration used, requires both temperature and humidity to be read + + this->status_clear_warning(); + if (this->write(&HDC1080_CMD_TEMPERATURE, 1) != i2c::ERROR_OK) { this->status_set_warning(); return; } - delay(20); - if (this->read(reinterpret_cast(&raw_temp), 2) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - raw_temp = i2c::i2ctohs(raw_temp); - float temp = raw_temp * 0.0025177f - 40.0f; // raw * 2^-16 * 165 - 40 - this->temperature_->publish_state(temp); - uint16_t raw_humidity; - if (this->write(&HDC1080_CMD_HUMIDITY, 1) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - delay(20); - if (this->read(reinterpret_cast(&raw_humidity), 2) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - raw_humidity = i2c::i2ctohs(raw_humidity); - float humidity = raw_humidity * 0.001525879f; // raw * 2^-16 * 100 - this->humidity_->publish_state(humidity); + this->set_timeout(20, [this]() { + uint16_t raw_temperature; + if (this->read(reinterpret_cast(&raw_temperature), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } - ESP_LOGD(TAG, "Got temperature=%.1f°C humidity=%.1f%%", temp, humidity); - this->status_clear_warning(); + if (this->temperature_ != nullptr) { + raw_temperature = i2c::i2ctohs(raw_temperature); + float temperature = raw_temperature * 0.0025177f - 40.0f; // raw * 2^-16 * 165 - 40 + this->temperature_->publish_state(temperature); + } + + if (this->write(&HDC1080_CMD_HUMIDITY, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + + this->set_timeout(20, [this]() { + uint16_t raw_humidity; + if (this->read(reinterpret_cast(&raw_humidity), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + + if (this->humidity_ != nullptr) { + raw_humidity = i2c::i2ctohs(raw_humidity); + float humidity = raw_humidity * 0.001525879f; // raw * 2^-16 * 100 + this->humidity_->publish_state(humidity); + } + }); + }); } -float HDC1080Component::get_setup_priority() const { return setup_priority::DATA; } } // namespace hdc1080 } // namespace esphome diff --git a/esphome/components/hdc1080/hdc1080.h b/esphome/components/hdc1080/hdc1080.h index 2ff7b6dc33..7ad0764f1f 100644 --- a/esphome/components/hdc1080/hdc1080.h +++ b/esphome/components/hdc1080/hdc1080.h @@ -12,13 +12,11 @@ class HDC1080Component : public PollingComponent, public i2c::I2CDevice { void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } - /// Setup the sensor and check for connection. void setup() override; void dump_config() override; - /// Retrieve the latest sensor values. This operation takes approximately 16ms. void update() override; - float get_setup_priority() const override; + float get_setup_priority() const override { return setup_priority::DATA; } protected: sensor::Sensor *temperature_{nullptr}; From db1aa823506662c1c5cb93bdb09dac6bb2befa1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Oct 2025 15:33:14 +0200 Subject: [PATCH 10/99] [core] Fix ComponentIterator alignment for 32-bit platforms (#10969) --- esphome/core/component_iterator.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index fdc30485bc..641d42898a 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -168,8 +168,9 @@ class ComponentIterator { UPDATE, #endif MAX, - } state_{IteratorState::NONE}; + }; uint16_t at_{0}; // Supports up to 65,535 entities per type + IteratorState state_{IteratorState::NONE}; bool include_internal_{false}; template From de21c61b6adc70ce39055c2827aacc33a646cbd6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Oct 2025 15:33:30 +0200 Subject: [PATCH 11/99] [logger] Optimize log formatting performance (35-72% faster) (#10960) --- esphome/components/logger/logger.h | 113 +++++++++++++++++++---------- 1 file changed, 74 insertions(+), 39 deletions(-) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index b5fb15d347..7d4c14df0b 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -36,29 +36,31 @@ struct device; namespace esphome::logger { -// Color and letter constants for log levels -static const char *const LOG_LEVEL_COLORS[] = { - "", // NONE - ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), // ERROR - ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_YELLOW), // WARNING - ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GREEN), // INFO - ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_MAGENTA), // CONFIG - ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_CYAN), // DEBUG - ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GRAY), // VERBOSE - ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_WHITE), // VERY_VERBOSE +// ANSI color code last digit (30-38 range, store only last digit to save RAM) +static constexpr char LOG_LEVEL_COLOR_DIGIT[] = { + '\0', // NONE + '1', // ERROR (31 = red) + '3', // WARNING (33 = yellow) + '2', // INFO (32 = green) + '5', // CONFIG (35 = magenta) + '6', // DEBUG (36 = cyan) + '7', // VERBOSE (37 = gray) + '8', // VERY_VERBOSE (38 = white) }; -static const char *const LOG_LEVEL_LETTERS[] = { - "", // NONE - "E", // ERROR - "W", // WARNING - "I", // INFO - "C", // CONFIG - "D", // DEBUG - "V", // VERBOSE - "VV", // VERY_VERBOSE +static constexpr char LOG_LEVEL_LETTER_CHARS[] = { + '\0', // NONE + 'E', // ERROR + 'W', // WARNING + 'I', // INFO + 'C', // CONFIG + 'D', // DEBUG + 'V', // VERBOSE (VERY_VERBOSE uses two 'V's) }; +// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin) +static constexpr uint16_t MAX_HEADER_SIZE = 128; + #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) /** Enum for logging UART selection * @@ -215,14 +217,6 @@ class Logger : public Component { } } - // Format string to explicit buffer with varargs - 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); - va_end(arg); - } - #ifndef USE_HOST const LogString *get_uart_selection_(); #endif @@ -318,26 +312,67 @@ class Logger : public Component { } #endif + static inline void copy_string(char *buffer, uint16_t &pos, const char *str) { + const size_t len = strlen(str); + // Intentionally no null terminator, building larger string + memcpy(buffer + pos, str, len); // NOLINT(bugprone-not-null-terminated-result) + pos += len; + } + + static inline void write_ansi_color_for_level(char *buffer, uint16_t &pos, uint8_t level) { + if (level == 0) + return; + // Construct ANSI escape sequence: "\033[{bold};3{color}m" + // Example: "\033[1;31m" for ERROR (bold red) + buffer[pos++] = '\033'; + buffer[pos++] = '['; + buffer[pos++] = (level == 1) ? '1' : '0'; // Only ERROR is bold + buffer[pos++] = ';'; + buffer[pos++] = '3'; + buffer[pos++] = LOG_LEVEL_COLOR_DIGIT[level]; + buffer[pos++] = 'm'; + } + 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 - // uint8_t level is already bounded 0-255, just ensure it's <= 7 - if (level > 7) - level = 7; + uint16_t pos = *buffer_at; + // Early return if insufficient space - intentionally don't update buffer_at to prevent partial writes + if (pos + MAX_HEADER_SIZE > buffer_size) + return; - const char *color = esphome::logger::LOG_LEVEL_COLORS[level]; - const char *letter = esphome::logger::LOG_LEVEL_LETTERS[level]; + // Construct: [LEVEL][tag:line]: + write_ansi_color_for_level(buffer, pos, level); + buffer[pos++] = '['; + if (level != 0) { + if (level >= 7) { + buffer[pos++] = 'V'; // VERY_VERBOSE = "VV" + buffer[pos++] = 'V'; + } else { + buffer[pos++] = LOG_LEVEL_LETTER_CHARS[level]; + } + } + buffer[pos++] = ']'; + buffer[pos++] = '['; + copy_string(buffer, pos, tag); + buffer[pos++] = ':'; + buffer[pos++] = '0' + (line / 100) % 10; + buffer[pos++] = '0' + (line / 10) % 10; + buffer[pos++] = '0' + line % 10; + buffer[pos++] = ']'; #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) if (thread_name != nullptr) { - // Non-main task with thread name - this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line, - ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), thread_name, color); - return; + write_ansi_color_for_level(buffer, pos, 1); // Always use bold red for thread name + buffer[pos++] = '['; + copy_string(buffer, pos, thread_name); + buffer[pos++] = ']'; + write_ansi_color_for_level(buffer, pos, level); // Restore original color } #endif - // Main task or non ESP32/LibreTiny platform - this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]: ", color, letter, tag, line); + + buffer[pos++] = ':'; + buffer[pos++] = ' '; + *buffer_at = pos; } inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, From 1deb79a24b0a0d52e036b9a6d17651c1e59351ce Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 2 Oct 2025 02:36:17 +1300 Subject: [PATCH 12/99] [core] Add some types to `loader.py` (#10967) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/loader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/loader.py b/esphome/loader.py index 7b2472521a..ec2f5101da 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -192,7 +192,7 @@ def install_custom_components_meta_finder(): install_meta_finder(custom_components_dir) -def _lookup_module(domain, exception): +def _lookup_module(domain: str, exception: bool) -> ComponentManifest | None: if domain in _COMPONENT_CACHE: return _COMPONENT_CACHE[domain] @@ -219,16 +219,16 @@ def _lookup_module(domain, exception): return manif -def get_component(domain, exception=False): +def get_component(domain: str, exception: bool = False) -> ComponentManifest | None: assert "." not in domain return _lookup_module(domain, exception) -def get_platform(domain, platform): +def get_platform(domain: str, platform: str) -> ComponentManifest | None: full = f"{platform}.{domain}" return _lookup_module(full, False) -_COMPONENT_CACHE = {} +_COMPONENT_CACHE: dict[str, ComponentManifest] = {} CORE_COMPONENTS_PATH = (Path(__file__).parent / "components").resolve() _COMPONENT_CACHE["esphome"] = ComponentManifest(esphome.core.config) From 08afc3030a90394deb5f3bc80bb933d6a1497cf2 Mon Sep 17 00:00:00 2001 From: Carl Reid <33623601+carlreid@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:47:32 +0200 Subject: [PATCH 13/99] [psram] raise instead of returning invalid object (#10954) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- tests/component_tests/psram/test_psram.py | 194 ++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 tests/component_tests/psram/test_psram.py diff --git a/tests/component_tests/psram/test_psram.py b/tests/component_tests/psram/test_psram.py new file mode 100644 index 0000000000..3e40a8d192 --- /dev/null +++ b/tests/component_tests/psram/test_psram.py @@ -0,0 +1,194 @@ +"""Tests for PSRAM component.""" + +from typing import Any + +import pytest + +from esphome.components.esp32.const import ( + KEY_VARIANT, + VARIANT_ESP32, + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32H2, + VARIANT_ESP32P4, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) +import esphome.config_validation as cv +from esphome.const import CONF_ESPHOME, PlatformFramework +from tests.component_tests.types import SetCoreConfigCallable + +UNSUPPORTED_PSRAM_VARIANTS = [ + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32H2, +] + +SUPPORTED_PSRAM_VARIANTS = [ + VARIANT_ESP32, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + VARIANT_ESP32P4, +] + + +@pytest.mark.parametrize( + ("config", "error_match"), + [ + pytest.param( + {}, + r"PSRAM is not supported on this chip", + id="psram_not_supported", + ), + ], +) +@pytest.mark.parametrize("variant", UNSUPPORTED_PSRAM_VARIANTS) +def test_psram_configuration_errors_unsupported_variants( + config: Any, + error_match: str, + variant: str, + set_core_config: SetCoreConfigCallable, +) -> None: + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_VARIANT: variant}, + full_config={CONF_ESPHOME: {}}, + ) + """Test detection of invalid PSRAM configuration on unsupported variants.""" + from esphome.components.psram import CONFIG_SCHEMA + + with pytest.raises(cv.Invalid, match=error_match): + CONFIG_SCHEMA(config) + + +@pytest.mark.parametrize("variant", SUPPORTED_PSRAM_VARIANTS) +def test_psram_configuration_valid_supported_variants( + variant: str, + set_core_config: SetCoreConfigCallable, +) -> None: + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_VARIANT: variant}, + full_config={ + CONF_ESPHOME: {}, + "esp32": { + "variant": variant, + "cpu_frequency": "160MHz", + "framework": {"type": "esp-idf"}, + }, + }, + ) + """Test that PSRAM configuration is valid on supported variants.""" + from esphome.components.psram import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA + + # This should not raise an exception + config = CONFIG_SCHEMA({}) + FINAL_VALIDATE_SCHEMA(config) + + +def _setup_psram_final_validation_test( + esp32_config: dict, + set_core_config: SetCoreConfigCallable, + set_component_config: Any, +) -> str: + """Helper function to set up ESP32 configuration for PSRAM final validation tests.""" + # Use ESP32S3 for schema validation to allow all options, then override for final validation + schema_variant = "ESP32S3" + final_variant = esp32_config.get("variant", "ESP32S3") + full_esp32_config = { + "variant": final_variant, + "cpu_frequency": esp32_config.get("cpu_frequency", "240MHz"), + "framework": {"type": "esp-idf"}, + } + + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_VARIANT: schema_variant}, + full_config={ + CONF_ESPHOME: {}, + "esp32": full_esp32_config, + }, + ) + set_component_config("esp32", full_esp32_config) + + return final_variant + + +@pytest.mark.parametrize( + ("config", "esp32_config", "expect_error", "error_match"), + [ + pytest.param( + {"speed": "120MHz"}, + {"cpu_frequency": "160MHz"}, + True, + r"PSRAM 120MHz requires 240MHz CPU frequency", + id="120mhz_requires_240mhz_cpu", + ), + pytest.param( + {"mode": "octal"}, + {"variant": "ESP32"}, + True, + r"Octal PSRAM is only supported on ESP32-S3", + id="octal_mode_only_esp32s3", + ), + pytest.param( + {"mode": "quad", "enable_ecc": True}, + {}, + True, + r"ECC is only available in octal mode", + id="ecc_only_in_octal_mode", + ), + pytest.param( + {"speed": "120MHZ"}, + {"cpu_frequency": "240MHZ"}, + False, + None, + id="120mhz_with_240mhz_cpu", + ), + pytest.param( + {"mode": "octal"}, + {"variant": "ESP32S3"}, + False, + None, + id="octal_mode_on_esp32s3", + ), + pytest.param( + {"mode": "octal", "enable_ecc": True}, + {"variant": "ESP32S3"}, + False, + None, + id="ecc_in_octal_mode", + ), + ], +) +def test_psram_final_validation( + config: Any, + esp32_config: dict, + expect_error: bool, + error_match: str | None, + set_core_config: SetCoreConfigCallable, + set_component_config: Any, +) -> None: + """Test PSRAM final validation for both error and valid cases.""" + from esphome.components.psram import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA + from esphome.core import CORE + + final_variant = _setup_psram_final_validation_test( + esp32_config, set_core_config, set_component_config + ) + + validated_config = CONFIG_SCHEMA(config) + + # Update CORE variant for final validation + CORE.data["esp32"][KEY_VARIANT] = final_variant + + if expect_error: + with pytest.raises(cv.Invalid, match=error_match): + FINAL_VALIDATE_SCHEMA(validated_config) + else: + # This should not raise an exception + FINAL_VALIDATE_SCHEMA(validated_config) From 8137d7600a30792f755ef018591d0ef25c0894d8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 1 Oct 2025 14:26:25 -0400 Subject: [PATCH 14/99] [rtttl] Fix warning (#10972) --- esphome/components/rtttl/rtttl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 5aedc74489..2c48105490 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -374,7 +374,7 @@ void Rtttl::loop() { this->last_note_ = millis(); } -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE static const LogString *state_to_string(State state) { switch (state) { case STATE_STOPPED: From 638c6cc14e2d64fc52ebdc29d76daf1f16ce92d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Oct 2025 20:26:47 +0200 Subject: [PATCH 15/99] [api] Reduce flash usage in user services by eliminating vector copy (#10971) --- esphome/components/api/user_services.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 5f040e8433..dba2d055bf 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -55,7 +55,7 @@ template class UserServiceBase : public UserServiceDescriptor { protected: virtual void execute(Ts... x) = 0; - template void execute_(std::vector args, seq type) { + template void execute_(const std::vector &args, seq type) { this->execute((get_execute_arg_value(args[S]))...); } From f2aa5a754cb4654045ee64db2026828cf119d459 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Oct 2025 01:44:30 +0200 Subject: [PATCH 16/99] [api] Remove ClientInfo::get_combined_info() to eliminate heap fragmentation (#10970) --- esphome/components/api/api_connection.cpp | 15 ++++++++------- esphome/components/api/api_connection.h | 11 ++--------- esphome/components/api/api_frame_helper.cpp | 3 ++- esphome/components/api/api_frame_helper_noise.cpp | 3 ++- .../components/api/api_frame_helper_plaintext.cpp | 3 ++- esphome/components/api/api_server.cpp | 3 ++- .../voice_assistant/voice_assistant.cpp | 5 +++-- 7 files changed, 21 insertions(+), 22 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 30b98803d1..2d12bf5f09 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -205,7 +205,8 @@ void APIConnection::loop() { // Disconnect if not responded within 2.5*keepalive 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()); + ESP_LOGW(TAG, "%s (%s) is unresponsive; disconnecting", this->client_info_.name.c_str(), + this->client_info_.peername.c_str()); } } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) { // Only send ping if we're not disconnecting @@ -255,7 +256,7 @@ bool APIConnection::send_disconnect_response(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->get_client_combined_info().c_str()); + ESP_LOGD(TAG, "%s (%s) disconnected", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); this->flags_.next_close = true; DisconnectResponse resp; return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); @@ -1385,7 +1386,7 @@ void APIConnection::complete_authentication_() { } this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); - ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); + ESP_LOGD(TAG, "%s (%s) connected", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); #ifdef USE_API_CLIENT_CONNECTED_TRIGGER this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername); #endif @@ -1609,12 +1610,12 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { #ifdef USE_API_PASSWORD void APIConnection::on_unauthenticated_access() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s access without authentication", this->get_client_combined_info().c_str()); + ESP_LOGD(TAG, "%s (%s) no authentication", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); } #endif void APIConnection::on_no_setup_connection() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s access without full connection", this->get_client_combined_info().c_str()); + ESP_LOGD(TAG, "%s (%s) no connection setup", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); } void APIConnection::on_fatal_error() { this->helper_->close(); @@ -1866,8 +1867,8 @@ void APIConnection::process_state_subscriptions_() { #endif // USE_API_HOMEASSISTANT_STATES void APIConnection::log_warning_(const LogString *message, APIError err) { - ESP_LOGW(TAG, "%s: %s %s errno=%d", this->get_client_combined_info().c_str(), LOG_STR_ARG(message), - LOG_STR_ARG(api_error_to_logstr(err)), errno); + ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->client_info_.name.c_str(), this->client_info_.peername.c_str(), + LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno); } void APIConnection::log_socket_operation_failed_(APIError err) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index cc7e4d6895..a21574f6d5 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -19,14 +19,6 @@ namespace esphome::api { struct ClientInfo { std::string name; // Client name from Hello message std::string peername; // IP:port from socket - - std::string get_combined_info() const { - if (name == peername) { - // Before Hello message, both are the same - return name; - } - return name + " (" + peername + ")"; - } }; // Keepalive timeout in milliseconds @@ -278,7 +270,8 @@ class APIConnection final : public APIServerConnection { bool try_to_clear_buffer(bool log_out_of_space); bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; - std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); } + const std::string &get_name() const { return this->client_info_.name; } + const std::string &get_peername() const { return this->client_info_.peername; } protected: // Helper function to handle authentication completion diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index a284e09c4a..a63199a5c4 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -13,7 +13,8 @@ namespace esphome::api { static const char *const TAG = "api.frame_helper"; -#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) +#define HELPER_LOG(msg, ...) \ + ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) #ifdef HELPER_LOG_PACKETS #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 0e49f93db5..ab27699f06 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -24,7 +24,8 @@ static const char *const PROLOGUE_INIT = "NoiseAPIInit"; #endif static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") -#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) +#define HELPER_LOG(msg, ...) \ + ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) #ifdef HELPER_LOG_PACKETS #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 859bb26630..ff72f3cb55 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -18,7 +18,8 @@ namespace esphome::api { static const char *const TAG = "api.plaintext"; -#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) +#define HELPER_LOG(msg, ...) \ + ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) #ifdef HELPER_LOG_PACKETS #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 7fbe0e27f3..a8fdb635cf 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -177,7 +177,8 @@ void APIServer::loop() { // 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()); + ESP_LOGW(TAG, "%s (%s): Network down; disconnect", client->client_info_.name.c_str(), + client->client_info_.peername.c_str()); } // Continue to process and clean up the clients below } diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index a0cf1a155b..7ece73994f 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -429,8 +429,9 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr if (this->api_client_ != nullptr) { ESP_LOGE(TAG, "Multiple API Clients attempting to connect to Voice Assistant"); - ESP_LOGE(TAG, "Current client: %s", this->api_client_->get_client_combined_info().c_str()); - ESP_LOGE(TAG, "New client: %s", client->get_client_combined_info().c_str()); + ESP_LOGE(TAG, "Current client: %s (%s)", this->api_client_->get_name().c_str(), + this->api_client_->get_peername().c_str()); + ESP_LOGE(TAG, "New client: %s (%s)", client->get_name().c_str(), client->get_peername().c_str()); return; } From 624868bb05dab31f2781af2ad0df74c149baabf1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 22:58:06 +0200 Subject: [PATCH 17/99] Bump github/codeql-action from 3.30.5 to 3.30.6 (#10985) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5453dae9a7..0a5fd68326 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 + uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 + uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6 with: category: "/language:${{matrix.language}}" From 5419b8bddbb2cc04b193fc806677fcc6f57c612e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Oct 2025 04:53:16 +0200 Subject: [PATCH 18/99] [ci] Fix pre-commit action to comply with pinned SHA security policy (#10990) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4f7f8bd82..bb038cb8aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -466,7 +466,7 @@ jobs: with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 + - uses: esphome/action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache env: SKIP: pylint,clang-tidy-hash - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 From ba0532cda7000d138570947a25af93dc537a75f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gr=C3=BCndel?= <45913260+ogruendel@users.noreply.github.com> Date: Fri, 3 Oct 2025 13:36:14 +0200 Subject: [PATCH 19/99] Fix UNIT_KILOVOLT_AMPS_REACTIVE constant definition (#10992) --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index ec583beeb6..7813b72bfa 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1169,7 +1169,7 @@ UNIT_KILOMETER = "km" UNIT_KILOMETER_PER_HOUR = "km/h" UNIT_KILOVOLT_AMPS = "kVA" UNIT_KILOVOLT_AMPS_HOURS = "kVAh" -UNIT_KILOVOLT_AMPS_REACTIVE = "kVAR" +UNIT_KILOVOLT_AMPS_REACTIVE = "kvar" UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kvarh" UNIT_KILOWATT = "kW" UNIT_KILOWATT_HOURS = "kWh" From 89c3340ef6f6c24b5fe4f6e90ef4549e36aeb1d9 Mon Sep 17 00:00:00 2001 From: mrtoy-me <118446898+mrtoy-me@users.noreply.github.com> Date: Fri, 3 Oct 2025 23:06:16 +1000 Subject: [PATCH 20/99] [mpr121] remove delay (#10963) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/mpr121/mpr121.cpp | 75 ++++++++++++++-------------- esphome/components/mpr121/mpr121.h | 2 +- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/esphome/components/mpr121/mpr121.cpp b/esphome/components/mpr121/mpr121.cpp index 074bc79ea2..60d8235b03 100644 --- a/esphome/components/mpr121/mpr121.cpp +++ b/esphome/components/mpr121/mpr121.cpp @@ -13,45 +13,46 @@ static const char *const TAG = "mpr121"; void MPR121Component::setup() { // soft reset device this->write_byte(MPR121_SOFTRESET, 0x63); - delay(100); // NOLINT - if (!this->write_byte(MPR121_ECR, 0x0)) { - this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(); - return; - } + this->set_timeout(100, [this]() { + if (!this->write_byte(MPR121_ECR, 0x0)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + // set touch sensitivity for all 12 channels + for (auto *channel : this->channels_) { + channel->setup(); + } + this->write_byte(MPR121_MHDR, 0x01); + this->write_byte(MPR121_NHDR, 0x01); + this->write_byte(MPR121_NCLR, 0x0E); + this->write_byte(MPR121_FDLR, 0x00); - // set touch sensitivity for all 12 channels - for (auto *channel : this->channels_) { - channel->setup(); - } - this->write_byte(MPR121_MHDR, 0x01); - this->write_byte(MPR121_NHDR, 0x01); - this->write_byte(MPR121_NCLR, 0x0E); - this->write_byte(MPR121_FDLR, 0x00); + this->write_byte(MPR121_MHDF, 0x01); + this->write_byte(MPR121_NHDF, 0x05); + this->write_byte(MPR121_NCLF, 0x01); + this->write_byte(MPR121_FDLF, 0x00); - this->write_byte(MPR121_MHDF, 0x01); - this->write_byte(MPR121_NHDF, 0x05); - this->write_byte(MPR121_NCLF, 0x01); - this->write_byte(MPR121_FDLF, 0x00); + this->write_byte(MPR121_NHDT, 0x00); + this->write_byte(MPR121_NCLT, 0x00); + this->write_byte(MPR121_FDLT, 0x00); - this->write_byte(MPR121_NHDT, 0x00); - this->write_byte(MPR121_NCLT, 0x00); - this->write_byte(MPR121_FDLT, 0x00); + this->write_byte(MPR121_DEBOUNCE, 0); + // default, 16uA charge current + this->write_byte(MPR121_CONFIG1, 0x10); + // 0.5uS encoding, 1ms period + this->write_byte(MPR121_CONFIG2, 0x20); - this->write_byte(MPR121_DEBOUNCE, 0); - // default, 16uA charge current - this->write_byte(MPR121_CONFIG1, 0x10); - // 0.5uS encoding, 1ms period - this->write_byte(MPR121_CONFIG2, 0x20); + // Write the Electrode Configuration Register + // * Highest 2 bits is "Calibration Lock", which we set to a value corresponding to 5 bits. + // * The 2 bits below is "Proximity Enable" and are left at 0. + // * The 4 least significant bits control how many electrodes are enabled. Electrodes are enabled + // as a range, starting at 0 up to the highest channel index used. + this->write_byte(MPR121_ECR, 0x80 | (this->max_touch_channel_ + 1)); - // Write the Electrode Configuration Register - // * Highest 2 bits is "Calibration Lock", which we set to a value corresponding to 5 bits. - // * The 2 bits below is "Proximity Enable" and are left at 0. - // * The 4 least significant bits control how many electrodes are enabled. Electrodes are enabled - // as a range, starting at 0 up to the highest channel index used. - this->write_byte(MPR121_ECR, 0x80 | (this->max_touch_channel_ + 1)); - - this->flush_gpio_(); + this->flush_gpio_(); + this->setup_complete_ = true; + }); } void MPR121Component::set_touch_debounce(uint8_t debounce) { @@ -73,15 +74,15 @@ void MPR121Component::dump_config() { case COMMUNICATION_FAILED: ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); break; - case WRONG_CHIP_STATE: - ESP_LOGE(TAG, "MPR121 has wrong default value for CONFIG2?"); - break; case NONE: default: break; } } void MPR121Component::loop() { + if (!this->setup_complete_) + return; + uint16_t val = 0; this->read_byte_16(MPR121_TOUCHSTATUS_L, &val); diff --git a/esphome/components/mpr121/mpr121.h b/esphome/components/mpr121/mpr121.h index eb2e2edc57..8f942f3e98 100644 --- a/esphome/components/mpr121/mpr121.h +++ b/esphome/components/mpr121/mpr121.h @@ -80,6 +80,7 @@ class MPR121Component : public Component, public i2c::I2CDevice { void pin_mode(uint8_t ionum, gpio::Flags flags); protected: + bool setup_complete_{false}; std::vector channels_{}; uint8_t debounce_{0}; uint8_t touch_threshold_{}; @@ -88,7 +89,6 @@ class MPR121Component : public Component, public i2c::I2CDevice { enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, - WRONG_CHIP_STATE, } error_code_{NONE}; bool flush_gpio_(); From 2b389bb8f2a3d02ede8b7cafa32cdf9351145382 Mon Sep 17 00:00:00 2001 From: mrtoy-me <118446898+mrtoy-me@users.noreply.github.com> Date: Fri, 3 Oct 2025 23:40:43 +1000 Subject: [PATCH 21/99] [sps30] remove delay (#10964) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/sps30/sps30.cpp | 30 +++++++++++++++++------------- esphome/components/sps30/sps30.h | 3 +++ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index b99bf416d6..dc14e3a610 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -45,24 +45,26 @@ void SPS30Component::setup() { } ESP_LOGV(TAG, " Serial number: %s", this->serial_number_); - bool result; if (this->fan_interval_.has_value()) { // override default value - result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value()); + this->result_ = + this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value()); } else { - result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS); - } - if (result) { - delay(20); - uint16_t secs[2]; - if (this->read_data(secs, 2)) { - this->fan_interval_ = secs[0] << 16 | secs[1]; - } + this->result_ = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS); } - this->status_clear_warning(); - this->skipped_data_read_cycles_ = 0; - this->start_continuous_measurement_(); + this->set_timeout(20, [this]() { + if (this->result_) { + uint16_t secs[2]; + if (this->read_data(secs, 2)) { + this->fan_interval_ = secs[0] << 16 | secs[1]; + } + } + this->status_clear_warning(); + this->skipped_data_read_cycles_ = 0; + this->start_continuous_measurement_(); + this->setup_complete_ = true; + }); }); } @@ -111,6 +113,8 @@ void SPS30Component::dump_config() { } void SPS30Component::update() { + if (!this->setup_complete_) + return; /// Check if warning flag active (sensor reconnected?) if (this->status_has_warning()) { ESP_LOGD(TAG, "Reconnecting"); diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h index 461a770ab6..cab5a075a0 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -30,9 +30,12 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri bool start_fan_cleaning(); protected: + bool result_{false}; + bool setup_complete_{false}; uint16_t raw_firmware_version_; char serial_number_[17] = {0}; /// Terminating NULL character uint8_t skipped_data_read_cycles_ = 0; + bool start_continuous_measurement_(); enum ErrorCode : uint8_t { From 14a23101f2ddf9e3473451d9779fe0b712d592a0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:35:55 -0400 Subject: [PATCH 22/99] [core] Fix MQTT import (#10982) --- esphome/__main__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 38edcb070f..b0f541f521 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -14,9 +14,11 @@ from typing import Protocol import argcomplete +# Note: Do not import modules from esphome.components here, as this would +# cause them to be loaded before external components are processed, resulting +# in the built-in version being used instead of the external component one. from esphome import const, writer, yaml_util import esphome.codegen as cg -from esphome.components.mqtt import CONF_DISCOVER_IP from esphome.config import iter_component_configs, read_config, strip_default_ids from esphome.const import ( ALLOWED_NAME_CHARS, @@ -240,6 +242,8 @@ def has_ota() -> bool: def has_mqtt_ip_lookup() -> bool: """Check if MQTT is available and IP lookup is supported.""" + from esphome.components.mqtt import CONF_DISCOVER_IP + if CONF_MQTT not in CORE.config: return False # Default Enabled From ca0e738799f62e8fc2170fb6dd2ba03e5f117355 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Oct 2025 10:50:21 -0500 Subject: [PATCH 23/99] [logger] Fix line number wrapping bug for files with >999 lines (#10979) --- esphome/components/logger/logger.h | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 7d4c14df0b..f0e0ed9a27 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -355,9 +355,18 @@ class Logger : public Component { buffer[pos++] = '['; copy_string(buffer, pos, tag); buffer[pos++] = ':'; - buffer[pos++] = '0' + (line / 100) % 10; - buffer[pos++] = '0' + (line / 10) % 10; - buffer[pos++] = '0' + line % 10; + // Format line number without modulo operations (passed by value, safe to mutate) + if (line > 999) [[unlikely]] { + int thousands = line / 1000; + buffer[pos++] = '0' + thousands; + line -= thousands * 1000; + } + int hundreds = line / 100; + int remainder = line - hundreds * 100; + int tens = remainder / 10; + buffer[pos++] = '0' + hundreds; + buffer[pos++] = '0' + tens; + buffer[pos++] = '0' + (remainder - tens * 10); buffer[pos++] = ']'; #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) From 6f8e82aeb682533d3a602dad1c05d3fe91e57860 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:27:29 -0500 Subject: [PATCH 24/99] Bump actions/stale from 10.0.0 to 10.1.0 (#11001) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index f57f0987ec..63a8ade37f 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Stale - uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 + uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 with: debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch remove-stale-when-updated: true From 2596b6096fcac0ace8ed4462da423a88806aa627 Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Fri, 3 Oct 2025 13:28:38 -0600 Subject: [PATCH 25/99] Fix log level selector when selecting levels above INFO (#10368) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../logger/select/logger_level_select.cpp | 13 ++++++------- .../components/logger/select/logger_level_select.h | 7 +++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/esphome/components/logger/select/logger_level_select.cpp b/esphome/components/logger/select/logger_level_select.cpp index d9c950ce3c..6d60a3ae47 100644 --- a/esphome/components/logger/select/logger_level_select.cpp +++ b/esphome/components/logger/select/logger_level_select.cpp @@ -3,11 +3,10 @@ namespace esphome::logger { void LoggerLevelSelect::publish_state(int level) { - auto value = this->at(level); - if (!value) { + const auto &option = this->at(level_to_index(level)); + if (!option) return; - } - Select::publish_state(value.value()); + Select::publish_state(option.value()); } void LoggerLevelSelect::setup() { @@ -16,10 +15,10 @@ void LoggerLevelSelect::setup() { } void LoggerLevelSelect::control(const std::string &value) { - auto level = this->index_of(value); - if (!level) + const auto index = this->index_of(value); + if (!index) return; - this->parent_->set_log_level(level.value()); + this->parent_->set_log_level(index_to_level(index.value())); } } // namespace esphome::logger diff --git a/esphome/components/logger/select/logger_level_select.h b/esphome/components/logger/select/logger_level_select.h index f31a6f6cdb..0631eca45d 100644 --- a/esphome/components/logger/select/logger_level_select.h +++ b/esphome/components/logger/select/logger_level_select.h @@ -3,11 +3,18 @@ #include "esphome/components/select/select.h" #include "esphome/core/component.h" #include "esphome/components/logger/logger.h" + namespace esphome::logger { class LoggerLevelSelect : public Component, public select::Select, public Parented { public: void publish_state(int level); void setup() override; void control(const std::string &value) override; + + protected: + // Convert log level to option index (skip CONFIG at level 4) + static uint8_t level_to_index(uint8_t level) { return (level > ESPHOME_LOG_LEVEL_CONFIG) ? level - 1 : level; } + // Convert option index to log level (skip CONFIG at level 4) + static uint8_t index_to_level(uint8_t index) { return (index >= ESPHOME_LOG_LEVEL_CONFIG) ? index + 1 : index; } }; } // namespace esphome::logger From d43b844e06b0a01f6ef0c550d7c95108fd09cbdf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:28:58 -0500 Subject: [PATCH 26/99] Bump ruff from 0.13.2 to 0.13.3 (#11000) 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 818f360860..3c1b888cfd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.13.2 + rev: v0.13.3 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 59ea77fd2d..f2be6f3a24 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.3.8 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.13.2 # also change in .pre-commit-config.yaml when updating +ruff==0.13.3 # 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 0a40a30e4a9a91bc6343bae74a7d4b6d20b83281 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sat, 4 Oct 2025 01:10:19 +0200 Subject: [PATCH 27/99] [esp32_can] support multiple CAN instances for platforms that support it (#10712) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/esp32_can/esp32_can.cpp | 26 +++++- esphome/components/esp32_can/esp32_can.h | 3 + .../esp32_can/test.esp32-c6-idf.yaml | 89 +++++++++++++++++++ 3 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 tests/components/esp32_can/test.esp32-c6-idf.yaml diff --git a/esphome/components/esp32_can/esp32_can.cpp b/esphome/components/esp32_can/esp32_can.cpp index b5e72497ce..252482dc5e 100644 --- a/esphome/components/esp32_can/esp32_can.cpp +++ b/esphome/components/esp32_can/esp32_can.cpp @@ -67,8 +67,16 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config } bool ESP32Can::setup_internal() { + static int next_twai_ctrl_num = 0; + if (next_twai_ctrl_num >= SOC_TWAI_CONTROLLER_NUM) { + ESP_LOGW(TAG, "Maximum number of esp32_can components created already"); + this->mark_failed(); + return false; + } + twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, TWAI_MODE_NORMAL); + g_config.controller_id = next_twai_ctrl_num++; if (this->tx_queue_len_.has_value()) { g_config.tx_queue_len = this->tx_queue_len_.value(); } @@ -86,14 +94,14 @@ bool ESP32Can::setup_internal() { } // Install TWAI driver - if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) { + if (twai_driver_install_v2(&g_config, &t_config, &f_config, &(this->twai_handle_)) != ESP_OK) { // Failed to install driver this->mark_failed(); return false; } // Start TWAI driver - if (twai_start() != ESP_OK) { + if (twai_start_v2(this->twai_handle_) != ESP_OK) { // Failed to start driver this->mark_failed(); return false; @@ -102,6 +110,11 @@ bool ESP32Can::setup_internal() { } canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { + if (this->twai_handle_ == nullptr) { + // not setup yet or setup failed + return canbus::ERROR_FAIL; + } + if (frame->can_data_length_code > canbus::CAN_MAX_DATA_LENGTH) { return canbus::ERROR_FAILTX; } @@ -124,7 +137,7 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { memcpy(message.data, frame->data, frame->can_data_length_code); } - if (twai_transmit(&message, this->tx_enqueue_timeout_ticks_) == ESP_OK) { + if (twai_transmit_v2(this->twai_handle_, &message, this->tx_enqueue_timeout_ticks_) == ESP_OK) { return canbus::ERROR_OK; } else { return canbus::ERROR_ALLTXBUSY; @@ -132,9 +145,14 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { } canbus::Error ESP32Can::read_message(struct canbus::CanFrame *frame) { + if (this->twai_handle_ == nullptr) { + // not setup yet or setup failed + return canbus::ERROR_FAIL; + } + twai_message_t message; - if (twai_receive(&message, 0) != ESP_OK) { + if (twai_receive_v2(this->twai_handle_, &message, 0) != ESP_OK) { return canbus::ERROR_NOMSG; } diff --git a/esphome/components/esp32_can/esp32_can.h b/esphome/components/esp32_can/esp32_can.h index 416f037083..dc44aceb36 100644 --- a/esphome/components/esp32_can/esp32_can.h +++ b/esphome/components/esp32_can/esp32_can.h @@ -5,6 +5,8 @@ #include "esphome/components/canbus/canbus.h" #include "esphome/core/component.h" +#include + namespace esphome { namespace esp32_can { @@ -29,6 +31,7 @@ class ESP32Can : public canbus::Canbus { TickType_t tx_enqueue_timeout_ticks_{}; optional tx_queue_len_{}; optional rx_queue_len_{}; + twai_handle_t twai_handle_{nullptr}; }; } // namespace esp32_can diff --git a/tests/components/esp32_can/test.esp32-c6-idf.yaml b/tests/components/esp32_can/test.esp32-c6-idf.yaml new file mode 100644 index 0000000000..6ef730c378 --- /dev/null +++ b/tests/components/esp32_can/test.esp32-c6-idf.yaml @@ -0,0 +1,89 @@ +esphome: + on_boot: + then: + - canbus.send: + # Extended ID explicit + canbus_id: esp32_internal_can + use_extended_id: true + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + - canbus.send: + # Standard ID by default + canbus_id: esp32_internal_can + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + - canbus.send: + # Extended ID explicit + canbus_id: esp32_internal_can_2 + use_extended_id: true + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + - canbus.send: + # Standard ID by default + canbus_id: esp32_internal_can_2 + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + +canbus: + - platform: esp32_can + id: esp32_internal_can + rx_pin: GPIO8 + tx_pin: GPIO7 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("canbus1", "canid 500 %s", b.c_str() ); + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus1", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus1", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus1", "supply_fan_flow"); + break; + // to be continued... + } + - platform: esp32_can + id: esp32_internal_can_2 + rx_pin: GPIO10 + tx_pin: GPIO9 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("canbus2", "canid 500 %s", b.c_str() ); + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus2", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus2", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus2", "supply_fan_flow"); + break; + // to be continued... + } From 0137954f2b48421c27a756d13d2df70c27fe0c76 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Oct 2025 18:20:00 -0500 Subject: [PATCH 28/99] [const] Move CONF_MAX_CONNECTIONS to const.py (#11007) --- esphome/components/api/__init__.py | 2 +- esphome/components/esp32_ble_tracker/__init__.py | 2 +- esphome/const.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index c91051ba20..e0d4fc8df2 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -14,6 +14,7 @@ from esphome.const import ( CONF_EVENT, CONF_ID, CONF_KEY, + CONF_MAX_CONNECTIONS, CONF_ON_CLIENT_CONNECTED, CONF_ON_CLIENT_DISCONNECTED, CONF_PASSWORD, @@ -60,7 +61,6 @@ CONF_CUSTOM_SERVICES = "custom_services" CONF_HOMEASSISTANT_SERVICES = "homeassistant_services" CONF_HOMEASSISTANT_STATES = "homeassistant_states" CONF_LISTEN_BACKLOG = "listen_backlog" -CONF_MAX_CONNECTIONS = "max_connections" def validate_encryption_key(value): diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 787fb9fb65..8ebee6b0b1 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -24,6 +24,7 @@ from esphome.const import ( CONF_INTERVAL, CONF_MAC_ADDRESS, CONF_MANUFACTURER_ID, + CONF_MAX_CONNECTIONS, CONF_ON_BLE_ADVERTISE, CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, CONF_ON_BLE_SERVICE_DATA_ADVERTISE, @@ -41,7 +42,6 @@ CODEOWNERS = ["@bdraco"] KEY_ESP32_BLE_TRACKER = "esp32_ble_tracker" KEY_USED_CONNECTION_SLOTS = "used_connection_slots" -CONF_MAX_CONNECTIONS = "max_connections" CONF_ESP32_BLE_ID = "esp32_ble_id" CONF_SCAN_PARAMETERS = "scan_parameters" CONF_WINDOW = "window" diff --git a/esphome/const.py b/esphome/const.py index 7813b72bfa..ee6eec32b1 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -542,6 +542,7 @@ CONF_MANUAL_IP = "manual_ip" CONF_MANUFACTURER_ID = "manufacturer_id" CONF_MASK_DISTURBER = "mask_disturber" CONF_MAX_BRIGHTNESS = "max_brightness" +CONF_MAX_CONNECTIONS = "max_connections" CONF_MAX_COOLING_RUN_TIME = "max_cooling_run_time" CONF_MAX_CURRENT = "max_current" CONF_MAX_DURATION = "max_duration" From 2eea674c04fa3d67a10fb811ef800a46c0e942d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Oct 2025 23:52:40 -0500 Subject: [PATCH 29/99] [json] Fix missing defines.h include causing PSRAM allocator to be unused (#11008) --- esphome/components/json/json_util.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index 0349833342..a8f452d7d0 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -2,6 +2,7 @@ #include +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #define ARDUINOJSON_ENABLE_STD_STRING 1 // NOLINT From 874db20b7dfaf88da7263791ce5a0409156e3344 Mon Sep 17 00:00:00 2001 From: mrtoy-me <118446898+mrtoy-me@users.noreply.github.com> Date: Sat, 4 Oct 2025 22:54:31 +1000 Subject: [PATCH 30/99] [mpr121] cleaner setup (#11013) --- esphome/components/mpr121/mpr121.cpp | 6 ++---- esphome/components/mpr121/mpr121.h | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/esphome/components/mpr121/mpr121.cpp b/esphome/components/mpr121/mpr121.cpp index 60d8235b03..5a8a8e7205 100644 --- a/esphome/components/mpr121/mpr121.cpp +++ b/esphome/components/mpr121/mpr121.cpp @@ -11,6 +11,7 @@ namespace mpr121 { static const char *const TAG = "mpr121"; void MPR121Component::setup() { + this->disable_loop(); // soft reset device this->write_byte(MPR121_SOFTRESET, 0x63); this->set_timeout(100, [this]() { @@ -51,7 +52,7 @@ void MPR121Component::setup() { this->write_byte(MPR121_ECR, 0x80 | (this->max_touch_channel_ + 1)); this->flush_gpio_(); - this->setup_complete_ = true; + this->enable_loop(); }); } @@ -80,9 +81,6 @@ void MPR121Component::dump_config() { } } void MPR121Component::loop() { - if (!this->setup_complete_) - return; - uint16_t val = 0; this->read_byte_16(MPR121_TOUCHSTATUS_L, &val); diff --git a/esphome/components/mpr121/mpr121.h b/esphome/components/mpr121/mpr121.h index 8f942f3e98..6dd2c38309 100644 --- a/esphome/components/mpr121/mpr121.h +++ b/esphome/components/mpr121/mpr121.h @@ -80,7 +80,6 @@ class MPR121Component : public Component, public i2c::I2CDevice { void pin_mode(uint8_t ionum, gpio::Flags flags); protected: - bool setup_complete_{false}; std::vector channels_{}; uint8_t debounce_{0}; uint8_t touch_threshold_{}; From 3f9924eac2cda69b044aa9a1af95520149065726 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Oct 2025 20:42:07 -0500 Subject: [PATCH 31/99] [core] Merge duplicate loops in mac_address_is_valid() (#11018) --- esphome/core/helpers.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index f1560711ef..85c33ea2d3 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -613,8 +613,6 @@ bool mac_address_is_valid(const uint8_t *mac) { if (mac[i] != 0) { is_all_zeros = false; } - } - for (uint8_t i = 0; i < 6; i++) { if (mac[i] != 0xFF) { is_all_ones = false; } From b1859c50bd9e2cd3a097e48d8a7b30d889230a1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Oct 2025 20:42:21 -0500 Subject: [PATCH 32/99] [api] Simplify message reading conditional (#11016) --- esphome/components/api/api_connection.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 2d12bf5f09..89da912aea 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -169,11 +169,8 @@ void APIConnection::loop() { } 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); - } + this->read_message(buffer.data_len, buffer.type, + buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr); if (this->flags_.remove) return; } From 6c7d92e726d6538d1c9a5ba8b2cdb674e2ff07f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Oct 2025 20:47:46 -0500 Subject: [PATCH 33/99] [ethernet] Consolidate error handling to reduce flash usage (#11019) --- esphome/components/ethernet/ethernet_component.cpp | 11 +++++++---- esphome/components/ethernet/ethernet_component.h | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 16f5903e3f..28043dd969 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -41,17 +41,20 @@ static const char *const TAG = "ethernet"; EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) { + ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err)); + this->mark_failed(); +} + #define ESPHL_ERROR_CHECK(err, message) \ if ((err) != ESP_OK) { \ - ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \ - this->mark_failed(); \ + this->log_error_and_mark_failed_(err, message); \ return; \ } #define ESPHL_ERROR_CHECK_RET(err, message, ret) \ if ((err) != ESP_OK) { \ - ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \ - this->mark_failed(); \ + this->log_error_and_mark_failed_(err, message); \ return ret; \ } diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 9a0da12241..c7cb0abb4c 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -106,6 +106,7 @@ class EthernetComponent : public Component { void start_connect_(); void finish_connect_(); void dump_connect_params_(); + void log_error_and_mark_failed_(esp_err_t err, const char *message); #ifdef USE_ETHERNET_KSZ8081 /// @brief Set `RMII Reference Clock Select` bit for KSZ8081. void ksz8081_set_clock_reference_(esp_eth_mac_t *mac); From 7b48fc292f1028d336d5696285f2e996cb1cd666 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 08:56:30 -0500 Subject: [PATCH 34/99] [api] Consolidate fatal error logging to reduce flash usage (#11015) --- esphome/components/api/api_connection.cpp | 22 ++++++---------------- esphome/components/api/api_connection.h | 7 +++++-- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 89da912aea..4f32112fa6 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -116,8 +116,7 @@ void APIConnection::start() { APIError err = this->helper_->init(); if (err != APIError::OK) { - on_fatal_error(); - this->log_warning_(LOG_STR("Helper init failed"), err); + this->fatal_error_with_log_(LOG_STR("Helper init failed"), err); return; } this->client_info_.peername = helper_->getpeername(); @@ -147,8 +146,7 @@ void APIConnection::loop() { APIError err = this->helper_->loop(); if (err != APIError::OK) { - on_fatal_error(); - this->log_socket_operation_failed_(err); + this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err); return; } @@ -163,8 +161,7 @@ void APIConnection::loop() { // No more data available break; } else if (err != APIError::OK) { - on_fatal_error(); - this->log_warning_(LOG_STR("Reading failed"), err); + this->fatal_error_with_log_(LOG_STR("Reading failed"), err); return; } else { this->last_traffic_ = now; @@ -1577,8 +1574,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { delay(0); APIError err = this->helper_->loop(); if (err != APIError::OK) { - on_fatal_error(); - this->log_socket_operation_failed_(err); + this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err); return false; } if (this->helper_->can_write_without_blocking()) @@ -1597,8 +1593,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { if (err == APIError::WOULD_BLOCK) return false; if (err != APIError::OK) { - on_fatal_error(); - this->log_warning_(LOG_STR("Packet write failed"), err); + this->fatal_error_with_log_(LOG_STR("Packet write failed"), err); return false; } // Do not set last_traffic_ on send @@ -1784,8 +1779,7 @@ void APIConnection::process_batch_() { APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf}, std::span(packet_info, packet_count)); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { - on_fatal_error(); - this->log_warning_(LOG_STR("Batch write failed"), err); + this->fatal_error_with_log_(LOG_STR("Batch write failed"), err); } #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1868,9 +1862,5 @@ void APIConnection::log_warning_(const LogString *message, APIError err) { LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno); } -void APIConnection::log_socket_operation_failed_(APIError err) { - this->log_warning_(LOG_STR("Socket operation failed"), err); -} - } // namespace esphome::api #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index a21574f6d5..ee9c81026c 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -732,8 +732,11 @@ class APIConnection final : public APIServerConnection { // Helper function to log API errors with errno void log_warning_(const LogString *message, APIError err); - // Specific helper for duplicated error message - void log_socket_operation_failed_(APIError err); + // Helper to handle fatal errors with logging + inline void fatal_error_with_log_(const LogString *message, APIError err) { + this->on_fatal_error(); + this->log_warning_(message, err); + } }; } // namespace esphome::api From 722c5a94f27310abaf7c7a5cffa4e7dd28868ac5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 5 Oct 2025 10:24:09 -0400 Subject: [PATCH 35/99] [sps30] Clean up (#10998) --- esphome/components/sps30/sps30.cpp | 10 +++++----- esphome/components/sps30/sps30.h | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index dc14e3a610..21a782e49a 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -45,16 +45,16 @@ void SPS30Component::setup() { } ESP_LOGV(TAG, " Serial number: %s", this->serial_number_); + bool result; if (this->fan_interval_.has_value()) { // override default value - this->result_ = - this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value()); + result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value()); } else { - this->result_ = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS); + result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS); } - this->set_timeout(20, [this]() { - if (this->result_) { + this->set_timeout(20, [this, result]() { + if (result) { uint16_t secs[2]; if (this->read_data(secs, 2)) { this->fan_interval_ = secs[0] << 16 | secs[1]; diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h index cab5a075a0..18847e16d9 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -30,7 +30,6 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri bool start_fan_cleaning(); protected: - bool result_{false}; bool setup_complete_{false}; uint16_t raw_firmware_version_; char serial_number_[17] = {0}; /// Terminating NULL character From 39d5cbc74a2284fe52d98b8d7e7b7f7b3a30cc48 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:20:40 -0500 Subject: [PATCH 36/99] [esp32_ble_server] Replace EventEmitter with direct callbacks to reduce memory usage (#10946) --- CODEOWNERS | 1 - .../components/esp32_ble_server/__init__.py | 2 +- .../esp32_ble_server/ble_characteristic.cpp | 17 +-- .../esp32_ble_server/ble_characteristic.h | 30 ++--- .../esp32_ble_server/ble_descriptor.cpp | 7 +- .../esp32_ble_server/ble_descriptor.h | 22 ++-- .../esp32_ble_server/ble_server.cpp | 12 +- .../components/esp32_ble_server/ble_server.h | 35 ++++-- .../ble_server_automations.cpp | 37 ++---- .../esp32_ble_server/ble_server_automations.h | 42 ++----- .../esp32_improv/esp32_improv_component.cpp | 14 +-- esphome/components/event_emitter/__init__.py | 5 - .../components/event_emitter/event_emitter.h | 117 ------------------ 13 files changed, 107 insertions(+), 234 deletions(-) delete mode 100644 esphome/components/event_emitter/__init__.py delete mode 100644 esphome/components/event_emitter/event_emitter.h diff --git a/CODEOWNERS b/CODEOWNERS index 3747acd2b5..0b9935faf7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -160,7 +160,6 @@ esphome/components/esp_ldo/* @clydebarrow esphome/components/espnow/* @jesserockz esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/event/* @nohat -esphome/components/event_emitter/* @Rapsssito esphome/components/exposure_notifications/* @OttoWinter esphome/components/ezo/* @ssieb esphome/components/ezo_pmp/* @carlos-sarmiento diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 9eab9647b3..10fa09fcc3 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -26,7 +26,7 @@ from esphome.const import ( from esphome.core import CORE from esphome.schema_extractors import SCHEMA_EXTRACT -AUTO_LOAD = ["esp32_ble", "bytebuffer", "event_emitter"] +AUTO_LOAD = ["esp32_ble", "bytebuffer"] CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"] DEPENDENCIES = ["esp32"] DOMAIN = "esp32_ble_server" diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index fabcc75321..d485d9fe2d 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -73,7 +73,7 @@ void BLECharacteristic::notify() { void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) { // If the descriptor is the CCCD descriptor, listen to its write event to know if the client wants to be notified if (descriptor->get_uuid() == ESPBTUUID::from_uint16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG)) { - descriptor->on(BLEDescriptorEvt::VectorEvt::ON_WRITE, [this](const std::vector &value, uint16_t conn_id) { + descriptor->on_write([this](std::span value, uint16_t conn_id) { if (value.size() != 2) return; uint16_t cccd = encode_uint16(value[1], value[0]); @@ -208,8 +208,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt if (!param->read.need_rsp) break; // For some reason you can request a read but not want a response - this->EventEmitter::emit_(BLECharacteristicEvt::EmptyEvt::ON_READ, - param->read.conn_id); + if (this->on_read_callback_) { + (*this->on_read_callback_)(param->read.conn_id); + } uint16_t max_offset = 22; @@ -277,8 +278,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt } if (!param->write.is_prep) { - this->EventEmitter, uint16_t>::emit_( - BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->write.conn_id); + if (this->on_write_callback_) { + (*this->on_write_callback_)(this->value_, param->write.conn_id); + } } break; @@ -289,8 +291,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt break; this->write_event_ = false; if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { - this->EventEmitter, uint16_t>::emit_( - BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->exec_write.conn_id); + if (this->on_write_callback_) { + (*this->on_write_callback_)(this->value_, param->exec_write.conn_id); + } } esp_err_t err = esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index 97b3af2a21..4a29683f41 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -2,10 +2,12 @@ #include "ble_descriptor.h" #include "esphome/components/esp32_ble/ble_uuid.h" -#include "esphome/components/event_emitter/event_emitter.h" #include "esphome/components/bytebuffer/bytebuffer.h" #include +#include +#include +#include #ifdef USE_ESP32 @@ -22,22 +24,10 @@ namespace esp32_ble_server { using namespace esp32_ble; using namespace bytebuffer; -using namespace event_emitter; class BLEService; -namespace BLECharacteristicEvt { -enum VectorEvt { - ON_WRITE, -}; - -enum EmptyEvt { - ON_READ, -}; -} // namespace BLECharacteristicEvt - -class BLECharacteristic : public EventEmitter, uint16_t>, - public EventEmitter { +class BLECharacteristic { public: BLECharacteristic(ESPBTUUID uuid, uint32_t properties); ~BLECharacteristic(); @@ -76,6 +66,15 @@ class BLECharacteristic : public EventEmitter, uint16_t)> &&callback) { + this->on_write_callback_ = + std::make_unique, uint16_t)>>(std::move(callback)); + } + void on_read(std::function &&callback) { + this->on_read_callback_ = std::make_unique>(std::move(callback)); + } + protected: bool write_event_{false}; BLEService *service_{}; @@ -98,6 +97,9 @@ class BLECharacteristic : public EventEmitter, uint16_t)>> on_write_callback_; + std::unique_ptr> on_read_callback_; + esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; enum State : uint8_t { diff --git a/esphome/components/esp32_ble_server/ble_descriptor.cpp b/esphome/components/esp32_ble_server/ble_descriptor.cpp index afbe579513..16941cca0f 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.cpp +++ b/esphome/components/esp32_ble_server/ble_descriptor.cpp @@ -74,9 +74,10 @@ void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_ break; this->value_.attr_len = param->write.len; memcpy(this->value_.attr_value, param->write.value, param->write.len); - this->emit_(BLEDescriptorEvt::VectorEvt::ON_WRITE, - std::vector(param->write.value, param->write.value + param->write.len), - param->write.conn_id); + if (this->on_write_callback_) { + (*this->on_write_callback_)(std::span(param->write.value, param->write.len), + param->write.conn_id); + } break; } default: diff --git a/esphome/components/esp32_ble_server/ble_descriptor.h b/esphome/components/esp32_ble_server/ble_descriptor.h index 8d3c22c5a1..425462a316 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.h +++ b/esphome/components/esp32_ble_server/ble_descriptor.h @@ -1,30 +1,26 @@ #pragma once #include "esphome/components/esp32_ble/ble_uuid.h" -#include "esphome/components/event_emitter/event_emitter.h" #include "esphome/components/bytebuffer/bytebuffer.h" #ifdef USE_ESP32 #include #include +#include +#include +#include namespace esphome { namespace esp32_ble_server { using namespace esp32_ble; using namespace bytebuffer; -using namespace event_emitter; class BLECharacteristic; -namespace BLEDescriptorEvt { -enum VectorEvt { - ON_WRITE, -}; -} // namespace BLEDescriptorEvt - -class BLEDescriptor : public EventEmitter, uint16_t> { +// Base class for BLE descriptors +class BLEDescriptor { public: BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100, bool read = true, bool write = true); virtual ~BLEDescriptor(); @@ -39,6 +35,12 @@ class BLEDescriptor : public EventEmitterstate_ == CREATED; } bool is_failed() { return this->state_ == FAILED; } + // Direct callback registration - only allocates when callback is set + void on_write(std::function, uint16_t)> &&callback) { + this->on_write_callback_ = + std::make_unique, uint16_t)>>(std::move(callback)); + } + protected: BLECharacteristic *characteristic_{nullptr}; ESPBTUUID uuid_; @@ -46,6 +48,8 @@ class BLEDescriptor : public EventEmitter, uint16_t)>> on_write_callback_; + esp_gatt_perm_t permissions_{}; enum State : uint8_t { diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 89299bb417..942be7e597 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -147,20 +147,28 @@ BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) { return nullptr; } +void BLEServer::dispatch_callbacks_(CallbackType type, uint16_t conn_id) { + for (auto &entry : this->callbacks_) { + if (entry.type == type) { + entry.callback(conn_id); + } + } +} + void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { switch (event) { case ESP_GATTS_CONNECT_EVT: { ESP_LOGD(TAG, "BLE Client connected"); this->add_client_(param->connect.conn_id); - this->emit_(BLEServerEvt::EmptyEvt::ON_CONNECT, param->connect.conn_id); + this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id); break; } case ESP_GATTS_DISCONNECT_EVT: { ESP_LOGD(TAG, "BLE Client disconnected"); this->remove_client_(param->disconnect.conn_id); this->parent_->advertising_start(); - this->emit_(BLEServerEvt::EmptyEvt::ON_DISCONNECT, param->disconnect.conn_id); + this->dispatch_callbacks_(CallbackType::ON_DISCONNECT, param->disconnect.conn_id); break; } case ESP_GATTS_REG_EVT: { diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index b5973ed099..48005b1346 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -13,6 +13,7 @@ #include #include #include +#include #ifdef USE_ESP32 @@ -24,18 +25,7 @@ namespace esp32_ble_server { using namespace esp32_ble; using namespace bytebuffer; -namespace BLEServerEvt { -enum EmptyEvt { - ON_CONNECT, - ON_DISCONNECT, -}; -} // namespace BLEServerEvt - -class BLEServer : public Component, - public GATTsEventHandler, - public BLEStatusEventHandler, - public Parented, - public EventEmitter { +class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEventHandler, public Parented { public: void setup() override; void loop() override; @@ -65,7 +55,25 @@ class BLEServer : public Component, void ble_before_disabled_event_handler() override; + // Direct callback registration - supports multiple callbacks + void on_connect(std::function &&callback) { + this->callbacks_.push_back({CallbackType::ON_CONNECT, std::move(callback)}); + } + void on_disconnect(std::function &&callback) { + this->callbacks_.push_back({CallbackType::ON_DISCONNECT, std::move(callback)}); + } + protected: + enum class CallbackType : uint8_t { + ON_CONNECT, + ON_DISCONNECT, + }; + + struct CallbackEntry { + CallbackType type; + std::function callback; + }; + struct ServiceEntry { ESPBTUUID uuid; uint8_t inst_id; @@ -76,6 +84,9 @@ class BLEServer : public Component, void add_client_(uint16_t conn_id) { this->clients_.insert(conn_id); } void remove_client_(uint16_t conn_id) { this->clients_.erase(conn_id); } + void dispatch_callbacks_(CallbackType type, uint16_t conn_id); + + std::vector callbacks_; std::vector manufacturer_data_{}; esp_gatt_if_t gatts_if_{0}; diff --git a/esphome/components/esp32_ble_server/ble_server_automations.cpp b/esphome/components/esp32_ble_server/ble_server_automations.cpp index 67e00a9bfe..0761de994a 100644 --- a/esphome/components/esp32_ble_server/ble_server_automations.cpp +++ b/esphome/components/esp32_ble_server/ble_server_automations.cpp @@ -14,9 +14,10 @@ Trigger, uint16_t> *BLETriggers::create_characteristic_on_w BLECharacteristic *characteristic) { Trigger, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) new Trigger, uint16_t>(); - characteristic->EventEmitter, uint16_t>::on( - BLECharacteristicEvt::VectorEvt::ON_WRITE, - [on_write_trigger](const std::vector &data, uint16_t id) { on_write_trigger->trigger(data, id); }); + characteristic->on_write([on_write_trigger](std::span data, uint16_t id) { + // Convert span to vector for trigger + on_write_trigger->trigger(std::vector(data.begin(), data.end()), id); + }); return on_write_trigger; } #endif @@ -25,9 +26,10 @@ Trigger, uint16_t> *BLETriggers::create_characteristic_on_w Trigger, uint16_t> *BLETriggers::create_descriptor_on_write_trigger(BLEDescriptor *descriptor) { Trigger, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) new Trigger, uint16_t>(); - descriptor->on( - BLEDescriptorEvt::VectorEvt::ON_WRITE, - [on_write_trigger](const std::vector &data, uint16_t id) { on_write_trigger->trigger(data, id); }); + descriptor->on_write([on_write_trigger](std::span data, uint16_t id) { + // Convert span to vector for trigger + on_write_trigger->trigger(std::vector(data.begin(), data.end()), id); + }); return on_write_trigger; } #endif @@ -35,8 +37,7 @@ Trigger, uint16_t> *BLETriggers::create_descriptor_on_write #ifdef USE_ESP32_BLE_SERVER_ON_CONNECT Trigger *BLETriggers::create_server_on_connect_trigger(BLEServer *server) { Trigger *on_connect_trigger = new Trigger(); // NOLINT(cppcoreguidelines-owning-memory) - server->on(BLEServerEvt::EmptyEvt::ON_CONNECT, - [on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); }); + server->on_connect([on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); }); return on_connect_trigger; } #endif @@ -44,38 +45,22 @@ Trigger *BLETriggers::create_server_on_connect_trigger(BLEServer *serv #ifdef USE_ESP32_BLE_SERVER_ON_DISCONNECT Trigger *BLETriggers::create_server_on_disconnect_trigger(BLEServer *server) { Trigger *on_disconnect_trigger = new Trigger(); // NOLINT(cppcoreguidelines-owning-memory) - server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT, - [on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); }); + server->on_disconnect([on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); }); return on_disconnect_trigger; } #endif #ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION void BLECharacteristicSetValueActionManager::set_listener(BLECharacteristic *characteristic, - EventEmitterListenerID listener_id, const std::function &pre_notify_listener) { // Find and remove existing listener for this characteristic auto *existing = this->find_listener_(characteristic); if (existing != nullptr) { - // Remove the previous listener - characteristic->EventEmitter::off(BLECharacteristicEvt::EmptyEvt::ON_READ, - existing->listener_id); - // Remove the pre-notify listener - this->off(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, existing->pre_notify_listener_id); // Remove from vector this->remove_listener_(characteristic); } - // Create a new listener for the pre-notify event - EventEmitterListenerID pre_notify_listener_id = - this->on(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, - [pre_notify_listener, characteristic](const BLECharacteristic *evt_characteristic) { - // Only call the pre-notify listener if the characteristic is the one we are interested in - if (characteristic == evt_characteristic) { - pre_notify_listener(); - } - }); // Save the entry to the vector - this->listeners_.push_back({characteristic, listener_id, pre_notify_listener_id}); + this->listeners_.push_back({characteristic, pre_notify_listener}); } BLECharacteristicSetValueActionManager::ListenerEntry *BLECharacteristicSetValueActionManager::find_listener_( diff --git a/esphome/components/esp32_ble_server/ble_server_automations.h b/esphome/components/esp32_ble_server/ble_server_automations.h index 8fcb5842c3..543b1153fc 100644 --- a/esphome/components/esp32_ble_server/ble_server_automations.h +++ b/esphome/components/esp32_ble_server/ble_server_automations.h @@ -4,7 +4,6 @@ #include "ble_characteristic.h" #include "ble_descriptor.h" -#include "esphome/components/event_emitter/event_emitter.h" #include "esphome/core/automation.h" #include @@ -18,10 +17,6 @@ namespace esp32_ble_server { namespace esp32_ble_server_automations { using namespace esp32_ble; -using namespace event_emitter; - -// Invalid listener ID constant - 0 is used as sentinel value in EventEmitter -static constexpr EventEmitterListenerID INVALID_LISTENER_ID = 0; class BLETriggers { public: @@ -41,38 +36,29 @@ class BLETriggers { }; #ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION -enum BLECharacteristicSetValueActionEvt { - PRE_NOTIFY, -}; - // Class to make sure only one BLECharacteristicSetValueAction is active at a time for each characteristic -class BLECharacteristicSetValueActionManager - : public EventEmitter { +class BLECharacteristicSetValueActionManager { public: // Singleton pattern static BLECharacteristicSetValueActionManager *get_instance() { static BLECharacteristicSetValueActionManager instance; return &instance; } - void set_listener(BLECharacteristic *characteristic, EventEmitterListenerID listener_id, - const std::function &pre_notify_listener); - EventEmitterListenerID get_listener(BLECharacteristic *characteristic) { + void set_listener(BLECharacteristic *characteristic, const std::function &pre_notify_listener); + bool has_listener(BLECharacteristic *characteristic) { return this->find_listener_(characteristic) != nullptr; } + void emit_pre_notify(BLECharacteristic *characteristic) { for (const auto &entry : this->listeners_) { if (entry.characteristic == characteristic) { - return entry.listener_id; + entry.pre_notify_listener(); + break; } } - return INVALID_LISTENER_ID; - } - void emit_pre_notify(BLECharacteristic *characteristic) { - this->emit_(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, characteristic); } private: struct ListenerEntry { BLECharacteristic *characteristic; - EventEmitterListenerID listener_id; - EventEmitterListenerID pre_notify_listener_id; + std::function pre_notify_listener; }; std::vector listeners_; @@ -87,24 +73,22 @@ template class BLECharacteristicSetValueAction : public Actionset_buffer(buffer.get_data()); } void play(Ts... x) override { // If the listener is already set, do nothing - if (BLECharacteristicSetValueActionManager::get_instance()->get_listener(this->parent_) == this->listener_id_) + if (BLECharacteristicSetValueActionManager::get_instance()->has_listener(this->parent_)) return; // Set initial value this->parent_->set_value(this->buffer_.value(x...)); // Set the listener for read events - this->listener_id_ = this->parent_->EventEmitter::on( - BLECharacteristicEvt::EmptyEvt::ON_READ, [this, x...](uint16_t id) { - // Set the value of the characteristic every time it is read - this->parent_->set_value(this->buffer_.value(x...)); - }); + this->parent_->on_read([this, x...](uint16_t id) { + // Set the value of the characteristic every time it is read + this->parent_->set_value(this->buffer_.value(x...)); + }); // Set the listener in the global manager so only one BLECharacteristicSetValueAction is set for each characteristic BLECharacteristicSetValueActionManager::get_instance()->set_listener( - this->parent_, this->listener_id_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); }); + this->parent_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); }); } protected: BLECharacteristic *parent_; - EventEmitterListenerID listener_id_; }; #endif // USE_ESP32_BLE_SERVER_SET_VALUE_ACTION diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index ca08ff0cca..f773083890 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -38,8 +38,7 @@ void ESP32ImprovComponent::setup() { }); } #endif - global_ble_server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT, - [this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); }); + global_ble_server->on_disconnect([this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); }); // Start with loop disabled - will be enabled by start() when needed this->disable_loop(); @@ -57,12 +56,11 @@ void ESP32ImprovComponent::setup_characteristics() { this->error_->add_descriptor(error_descriptor); this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE); - this->rpc_->EventEmitter, uint16_t>::on( - BLECharacteristicEvt::VectorEvt::ON_WRITE, [this](const std::vector &data, uint16_t id) { - if (!data.empty()) { - this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); - } - }); + this->rpc_->on_write([this](std::span data, uint16_t id) { + if (!data.empty()) { + this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); + } + }); BLEDescriptor *rpc_descriptor = new BLE2902(); this->rpc_->add_descriptor(rpc_descriptor); diff --git a/esphome/components/event_emitter/__init__.py b/esphome/components/event_emitter/__init__.py deleted file mode 100644 index fcbbf26f02..0000000000 --- a/esphome/components/event_emitter/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -CODEOWNERS = ["@Rapsssito"] - -# Allows event_emitter to be configured in yaml, to allow use of the C++ api. - -CONFIG_SCHEMA = {} diff --git a/esphome/components/event_emitter/event_emitter.h b/esphome/components/event_emitter/event_emitter.h deleted file mode 100644 index 74afde03c0..0000000000 --- a/esphome/components/event_emitter/event_emitter.h +++ /dev/null @@ -1,117 +0,0 @@ -#pragma once -#include -#include -#include - -#include "esphome/core/log.h" - -namespace esphome { -namespace event_emitter { - -using EventEmitterListenerID = uint32_t; -static constexpr EventEmitterListenerID INVALID_LISTENER_ID = 0; - -// EventEmitter class that can emit events with a specific name (it is highly recommended to use an enum class for this) -// and a list of arguments. Supports multiple listeners for each event. -template class EventEmitter { - public: - EventEmitterListenerID on(EvtType event, std::function listener) { - EventEmitterListenerID listener_id = this->get_next_id_(); - - // Find or create event entry - EventEntry *entry = this->find_or_create_event_(event); - entry->listeners.push_back({listener_id, listener}); - - return listener_id; - } - - void off(EvtType event, EventEmitterListenerID id) { - EventEntry *entry = this->find_event_(event); - if (entry == nullptr) - return; - - // Remove listener with given id - for (auto it = entry->listeners.begin(); it != entry->listeners.end(); ++it) { - if (it->id == id) { - // Swap with last and pop for efficient removal - *it = entry->listeners.back(); - entry->listeners.pop_back(); - - // Remove event entry if no more listeners - if (entry->listeners.empty()) { - this->remove_event_(event); - } - return; - } - } - } - - protected: - void emit_(EvtType event, Args... args) { - EventEntry *entry = this->find_event_(event); - if (entry == nullptr) - return; - - // Call all listeners for this event - for (const auto &listener : entry->listeners) { - listener.callback(args...); - } - } - - private: - struct Listener { - EventEmitterListenerID id; - std::function callback; - }; - - struct EventEntry { - EvtType event; - std::vector listeners; - }; - - EventEmitterListenerID get_next_id_() { - // Simple incrementing ID, wrapping around at max - EventEmitterListenerID next_id = (this->current_id_ + 1); - if (next_id == INVALID_LISTENER_ID) { - next_id = 1; - } - this->current_id_ = next_id; - return this->current_id_; - } - - EventEntry *find_event_(EvtType event) { - for (auto &entry : this->events_) { - if (entry.event == event) { - return &entry; - } - } - return nullptr; - } - - EventEntry *find_or_create_event_(EvtType event) { - EventEntry *entry = this->find_event_(event); - if (entry != nullptr) - return entry; - - // Create new event entry - this->events_.push_back({event, {}}); - return &this->events_.back(); - } - - void remove_event_(EvtType event) { - for (auto it = this->events_.begin(); it != this->events_.end(); ++it) { - if (it->event == event) { - // Swap with last and pop - *it = this->events_.back(); - this->events_.pop_back(); - return; - } - } - } - - std::vector events_; - EventEmitterListenerID current_id_ = 0; -}; - -} // namespace event_emitter -} // namespace esphome From 19439199ccd2d23c2071ef067f0c727542f1e65a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:25:04 -0500 Subject: [PATCH 37/99] [api] Add configurable send queue limit to prevent OOM crashes (#10973) --- esphome/components/api/__init__.py | 15 +++++++ esphome/components/api/api_frame_helper.cpp | 49 +++++++++++++-------- esphome/components/api/api_frame_helper.h | 12 +++-- esphome/core/defines.h | 1 + 4 files changed, 55 insertions(+), 22 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index e0d4fc8df2..8f0910b9a3 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -61,6 +61,7 @@ CONF_CUSTOM_SERVICES = "custom_services" CONF_HOMEASSISTANT_SERVICES = "homeassistant_services" CONF_HOMEASSISTANT_STATES = "homeassistant_states" CONF_LISTEN_BACKLOG = "listen_backlog" +CONF_MAX_SEND_QUEUE = "max_send_queue" def validate_encryption_key(value): @@ -183,6 +184,19 @@ CONFIG_SCHEMA = cv.All( host=8, # Abundant resources ln882x=8, # Moderate RAM ): cv.int_range(min=1, max=20), + # Maximum queued send buffers per connection before dropping connection + # Each buffer uses ~8-12 bytes overhead plus actual message size + # Platform defaults based on available RAM and typical message rates: + cv.SplitDefault( + CONF_MAX_SEND_QUEUE, + esp8266=5, # Limited RAM, need to fail fast + esp32=8, # More RAM, can buffer more + rp2040=5, # Limited RAM + bk72xx=8, # Moderate RAM + rtl87xx=8, # Moderate RAM + host=16, # Abundant resources + ln882x=8, # Moderate RAM + ): cv.int_range(min=1, max=64), } ).extend(cv.COMPONENT_SCHEMA), cv.rename_key(CONF_SERVICES, CONF_ACTIONS), @@ -205,6 +219,7 @@ async def to_code(config): cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG])) if CONF_MAX_CONNECTIONS in config: cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS])) + cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE]) # Set USE_API_SERVICES if any services are enabled if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index a63199a5c4..20f8fcaf61 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -81,7 +81,7 @@ const LogString *api_error_to_logstr(APIError err) { // Default implementation for loop - handles sending buffered data APIError APIFrameHelper::loop() { - if (!this->tx_buf_.empty()) { + if (this->tx_buf_count_ > 0) { APIError err = try_send_tx_buf_(); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { return err; @@ -103,9 +103,20 @@ APIError APIFrameHelper::handle_socket_write_error_() { // Helper method to buffer data from IOVs void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset) { - SendBuffer buffer; - buffer.size = total_write_len - offset; - buffer.data = std::make_unique(buffer.size); + // Check if queue is full + if (this->tx_buf_count_ >= API_MAX_SEND_QUEUE) { + HELPER_LOG("Send queue full (%u buffers), dropping connection", this->tx_buf_count_); + this->state_ = State::FAILED; + return; + } + + uint16_t buffer_size = total_write_len - offset; + auto &buffer = this->tx_buf_[this->tx_buf_tail_]; + buffer = std::make_unique(SendBuffer{ + .data = std::make_unique(buffer_size), + .size = buffer_size, + .offset = 0, + }); uint16_t to_skip = offset; uint16_t write_pos = 0; @@ -118,12 +129,15 @@ void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, // Include this segment (partially or fully) const uint8_t *src = reinterpret_cast(iov[i].iov_base) + to_skip; uint16_t len = static_cast(iov[i].iov_len) - to_skip; - std::memcpy(buffer.data.get() + write_pos, src, len); + std::memcpy(buffer->data.get() + write_pos, src, len); write_pos += len; to_skip = 0; } } - this->tx_buf_.push_back(std::move(buffer)); + + // Update circular buffer tracking + this->tx_buf_tail_ = (this->tx_buf_tail_ + 1) % API_MAX_SEND_QUEUE; + this->tx_buf_count_++; } // This method writes data to socket or buffers it @@ -141,7 +155,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_ #endif // Try to send any existing buffered data first if there is any - if (!this->tx_buf_.empty()) { + if (this->tx_buf_count_ > 0) { APIError send_result = try_send_tx_buf_(); // If real error occurred (not just WOULD_BLOCK), return it if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) { @@ -150,7 +164,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_ // If there is still data in the buffer, we can't send, buffer // the new data and return - if (!this->tx_buf_.empty()) { + if (this->tx_buf_count_ > 0) { this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0); return APIError::OK; // Success, data buffered } @@ -178,32 +192,31 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_ } // Common implementation for trying to send buffered data -// IMPORTANT: Caller MUST ensure tx_buf_ is not empty before calling this method +// IMPORTANT: Caller MUST ensure tx_buf_count_ > 0 before calling this method APIError APIFrameHelper::try_send_tx_buf_() { // Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check - bool tx_buf_empty = false; - while (!tx_buf_empty) { + while (this->tx_buf_count_ > 0) { // Get the first buffer in the queue - SendBuffer &front_buffer = this->tx_buf_.front(); + SendBuffer *front_buffer = this->tx_buf_[this->tx_buf_head_].get(); // Try to send the remaining data in this buffer - ssize_t sent = this->socket_->write(front_buffer.current_data(), front_buffer.remaining()); + ssize_t sent = this->socket_->write(front_buffer->current_data(), front_buffer->remaining()); if (sent == -1) { return this->handle_socket_write_error_(); } else if (sent == 0) { // Nothing sent but not an error return APIError::WOULD_BLOCK; - } else if (static_cast(sent) < front_buffer.remaining()) { + } else if (static_cast(sent) < front_buffer->remaining()) { // Partially sent, update offset // Cast to ensure no overflow issues with uint16_t - front_buffer.offset += static_cast(sent); + front_buffer->offset += static_cast(sent); return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer } else { // Buffer completely sent, remove it from the queue - this->tx_buf_.pop_front(); - // Update empty status for the loop condition - tx_buf_empty = this->tx_buf_.empty(); + this->tx_buf_[this->tx_buf_head_].reset(); + this->tx_buf_head_ = (this->tx_buf_head_ + 1) % API_MAX_SEND_QUEUE; + this->tx_buf_count_--; // Continue loop to try sending the next buffer } } diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index c11d701ffe..3184250e8c 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -1,7 +1,8 @@ #pragma once +#include #include -#include #include +#include #include #include #include @@ -79,7 +80,7 @@ class APIFrameHelper { virtual APIError init() = 0; virtual APIError loop(); virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; - bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } + bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; } std::string getpeername() { return socket_->getpeername(); } int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); } APIError close() { @@ -161,7 +162,7 @@ class APIFrameHelper { }; // Containers (size varies, but typically 12+ bytes on 32-bit) - std::deque tx_buf_; + std::array, API_MAX_SEND_QUEUE> tx_buf_; std::vector reusable_iovs_; std::vector rx_buf_; @@ -174,7 +175,10 @@ class APIFrameHelper { State state_{State::INITIALIZE}; uint8_t frame_header_padding_{0}; uint8_t frame_footer_size_{0}; - // 5 bytes total, 3 bytes padding + uint8_t tx_buf_head_{0}; + uint8_t tx_buf_tail_{0}; + uint8_t tx_buf_count_{0}; + // 8 bytes total, 0 bytes padding // Common initialization for both plaintext and noise protocols APIError init_common_(); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 7fc42ea334..5516b06040 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -115,6 +115,7 @@ #define USE_API_NOISE #define USE_API_PLAINTEXT #define USE_API_SERVICES +#define API_MAX_SEND_QUEUE 8 #define USE_MD5 #define USE_SHA256 #define USE_MQTT From 0fd71ca2111da9030248a80b9e8c6b2df8c801ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:30:17 -0500 Subject: [PATCH 38/99] [mdns][openthread] Use StaticVector for services storage with compile-time capacity (#10976) --- esphome/components/mdns/__init__.py | 17 +++++++- esphome/components/mdns/mdns_component.cpp | 43 ++++---------------- esphome/components/mdns/mdns_component.h | 14 +++---- esphome/components/openthread/openthread.cpp | 9 ++-- esphome/components/openthread/openthread.h | 1 - esphome/core/defines.h | 1 + esphome/core/helpers.h | 10 +++++ 7 files changed, 44 insertions(+), 51 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index a84fe5a249..ce0241677d 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -17,6 +17,11 @@ from esphome.coroutine import CoroPriority CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] +# Components that create mDNS services at runtime +# IMPORTANT: If you add a new component here, you must also update the corresponding +# #ifdef blocks in mdns_component.cpp compile_records_() method +COMPONENTS_WITH_MDNS_SERVICES = ("api", "prometheus", "web_server") + mdns_ns = cg.esphome_ns.namespace("mdns") MDNSComponent = mdns_ns.class_("MDNSComponent", cg.Component) MDNSTXTRecord = mdns_ns.struct("MDNSTXTRecord") @@ -91,12 +96,20 @@ async def to_code(config): cg.add_define("USE_MDNS") - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) + # Calculate compile-time service count + service_count = sum( + 1 for key in COMPONENTS_WITH_MDNS_SERVICES if key in CORE.config + ) + len(config[CONF_SERVICES]) if config[CONF_SERVICES]: cg.add_define("USE_MDNS_EXTRA_SERVICES") + # Ensure at least 1 service (fallback service) + cg.add_define("MDNS_SERVICE_COUNT", max(1, service_count)) + + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + for service in config[CONF_SERVICES]: txt = [ cg.StructInitializer( diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 5d9788198f..eed2516c6a 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -74,32 +74,12 @@ MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread"); void MDNSComponent::compile_records_() { this->hostname_ = App.get_name(); - // Calculate exact capacity needed for services vector - size_t services_count = 0; -#ifdef USE_API - if (api::global_api_server != nullptr) { - services_count++; - } -#endif -#ifdef USE_PROMETHEUS - services_count++; -#endif -#ifdef USE_WEBSERVER - services_count++; -#endif -#ifdef USE_MDNS_EXTRA_SERVICES - services_count += this->services_extra_.size(); -#endif - // Reserve for fallback service if needed - if (services_count == 0) { - services_count = 1; - } - this->services_.reserve(services_count); + // IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES + // in mdns/__init__.py. If you add a new service here, update both locations. #ifdef USE_API if (api::global_api_server != nullptr) { - this->services_.emplace_back(); - auto &service = this->services_.back(); + auto &service = this->services_.emplace_next(); service.service_type = MDNS_STR(SERVICE_ESPHOMELIB); service.proto = MDNS_STR(SERVICE_TCP); service.port = api::global_api_server->get_port(); @@ -178,30 +158,23 @@ void MDNSComponent::compile_records_() { #endif // USE_API #ifdef USE_PROMETHEUS - this->services_.emplace_back(); - auto &prom_service = this->services_.back(); + auto &prom_service = this->services_.emplace_next(); prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS); prom_service.proto = MDNS_STR(SERVICE_TCP); prom_service.port = USE_WEBSERVER_PORT; #endif #ifdef USE_WEBSERVER - this->services_.emplace_back(); - auto &web_service = this->services_.back(); + auto &web_service = this->services_.emplace_next(); web_service.service_type = MDNS_STR(SERVICE_HTTP); web_service.proto = MDNS_STR(SERVICE_TCP); web_service.port = USE_WEBSERVER_PORT; #endif -#ifdef USE_MDNS_EXTRA_SERVICES - this->services_.insert(this->services_.end(), this->services_extra_.begin(), this->services_extra_.end()); -#endif - #if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_WEBSERVER) && !defined(USE_MDNS_EXTRA_SERVICES) // Publish "http" service if not using native API or any other services // This is just to have *some* mDNS service so that .local resolution works - this->services_.emplace_back(); - auto &fallback_service = this->services_.back(); + auto &fallback_service = this->services_.emplace_next(); fallback_service.service_type = "_http"; fallback_service.proto = "_tcp"; fallback_service.port = USE_WEBSERVER_PORT; @@ -214,7 +187,7 @@ void MDNSComponent::dump_config() { "mDNS:\n" " Hostname: %s", this->hostname_.c_str()); -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGV(TAG, " Services:"); for (const auto &service : this->services_) { ESP_LOGV(TAG, " - %s, %s, %d", service.service_type.c_str(), service.proto.c_str(), @@ -227,8 +200,6 @@ void MDNSComponent::dump_config() { #endif } -std::vector MDNSComponent::get_services() { return this->services_; } - } // namespace mdns } // namespace esphome #endif diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index f87ef08bcd..e0e268c914 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -2,13 +2,16 @@ #include "esphome/core/defines.h" #ifdef USE_MDNS #include -#include #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/helpers.h" namespace esphome { namespace mdns { +// Service count is calculated at compile time by Python codegen +// MDNS_SERVICE_COUNT will always be defined + struct MDNSTXTRecord { std::string key; TemplatableValue value; @@ -36,18 +39,15 @@ class MDNSComponent : public Component { float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } #ifdef USE_MDNS_EXTRA_SERVICES - void add_extra_service(MDNSService service) { services_extra_.push_back(std::move(service)); } + void add_extra_service(MDNSService service) { this->services_.emplace_next() = std::move(service); } #endif - std::vector get_services(); + const StaticVector &get_services() const { return this->services_; } void on_shutdown() override; protected: -#ifdef USE_MDNS_EXTRA_SERVICES - std::vector services_extra_{}; -#endif - std::vector services_{}; + StaticVector services_{}; std::string hostname_; void compile_records_(); }; diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 5b5c113f83..57b972d195 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -143,11 +143,10 @@ void OpenThreadSrpComponent::setup() { return; } - // 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_LOGD(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size()); - for (const auto &service : this->mdns_services_) { + // Get mdns services and copy their data (strings are copied with strdup below) + const auto &mdns_services = this->mdns_->get_services(); + ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", mdns_services.size()); + for (const auto &service : mdns_services) { otSrpClientBuffersServiceEntry *entry = otSrpClientBuffersAllocateService(instance); if (!entry) { ESP_LOGW(TAG, "Failed to allocate service entry"); diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index a9aff78e56..5d139c633d 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -57,7 +57,6 @@ class OpenThreadSrpComponent : public Component { protected: esphome::mdns::MDNSComponent *mdns_{nullptr}; - std::vector mdns_services_; std::vector> memory_pool_; void *pool_alloc_(size_t size); }; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5516b06040..554e1ee13c 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -82,6 +82,7 @@ #define USE_LVGL_TILEVIEW #define USE_LVGL_TOUCHSCREEN #define USE_MDNS +#define MDNS_SERVICE_COUNT 3 #define USE_MEDIA_PLAYER #define USE_NEXTION_TFT_UPLOAD #define USE_NUMBER diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index a28718de5a..53e82abfef 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -127,6 +127,16 @@ template class StaticVector { } } + // Return reference to next element and increment count (with bounds checking) + T &emplace_next() { + if (count_ >= N) { + // Should never happen with proper size calculation + // Return reference to last element to avoid crash + return data_[N - 1]; + } + return data_[count_++]; + } + size_t size() const { return count_; } bool empty() const { return count_ == 0; } From 41c073a45111e4d1290f0ca452a31027b48d88ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:33:58 -0500 Subject: [PATCH 39/99] [lock] Replace std::set with bitmask (saves 388B flash + 23B RAM per lock) (#10977) --- esphome/components/copy/lock/copy_lock.cpp | 2 +- esphome/components/lock/lock.h | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/esphome/components/copy/lock/copy_lock.cpp b/esphome/components/copy/lock/copy_lock.cpp index 67a8acffec..25bd8c33ef 100644 --- a/esphome/components/copy/lock/copy_lock.cpp +++ b/esphome/components/copy/lock/copy_lock.cpp @@ -11,7 +11,7 @@ void CopyLock::setup() { traits.set_assumed_state(source_->traits.get_assumed_state()); traits.set_requires_code(source_->traits.get_requires_code()); - traits.set_supported_states(source_->traits.get_supported_states()); + traits.set_supported_states_mask(source_->traits.get_supported_states_mask()); traits.set_supports_open(source_->traits.get_supports_open()); this->publish_state(source_->state); diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 04c4cd71cd..9737569921 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -5,7 +5,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/preferences.h" -#include +#include namespace esphome { namespace lock { @@ -44,16 +44,22 @@ class LockTraits { bool get_assumed_state() const { return this->assumed_state_; } void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } - bool supports_state(LockState state) const { return supported_states_.count(state); } - std::set get_supported_states() const { return supported_states_; } - void set_supported_states(std::set states) { supported_states_ = std::move(states); } - void add_supported_state(LockState state) { supported_states_.insert(state); } + bool supports_state(LockState state) const { return supported_states_mask_ & (1 << state); } + void set_supported_states(std::initializer_list states) { + supported_states_mask_ = 0; + for (auto state : states) { + supported_states_mask_ |= (1 << state); + } + } + uint8_t get_supported_states_mask() const { return supported_states_mask_; } + void set_supported_states_mask(uint8_t mask) { supported_states_mask_ = mask; } + void add_supported_state(LockState state) { supported_states_mask_ |= (1 << state); } protected: bool supports_open_{false}; bool requires_code_{false}; bool assumed_state_{false}; - std::set supported_states_ = {LOCK_STATE_NONE, LOCK_STATE_LOCKED, LOCK_STATE_UNLOCKED}; + uint8_t supported_states_mask_{(1 << LOCK_STATE_NONE) | (1 << LOCK_STATE_LOCKED) | (1 << LOCK_STATE_UNLOCKED)}; }; /** This class is used to encode all control actions on a lock device. From 120a445abf41b84de646d7c006e92a911fcb59be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:37:47 -0500 Subject: [PATCH 40/99] [number] Reduce flash usage in NumberCall logging (#10983) --- esphome/components/number/number_call.cpp | 35 +++++++++++++++-------- esphome/components/number/number_call.h | 5 ++++ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/esphome/components/number/number_call.cpp b/esphome/components/number/number_call.cpp index 4219f85328..669dd65184 100644 --- a/esphome/components/number/number_call.cpp +++ b/esphome/components/number/number_call.cpp @@ -7,6 +7,17 @@ namespace number { static const char *const TAG = "number"; +// Helper functions to reduce code size for logging +void NumberCall::log_perform_warning_(const LogString *message) { + ESP_LOGW(TAG, "'%s': %s", this->parent_->get_name().c_str(), LOG_STR_ARG(message)); +} + +void NumberCall::log_perform_warning_value_range_(const LogString *comparison, const LogString *limit_type, float val, + float limit) { + ESP_LOGW(TAG, "'%s': %f %s %s %f", this->parent_->get_name().c_str(), val, LOG_STR_ARG(comparison), + LOG_STR_ARG(limit_type), limit); +} + NumberCall &NumberCall::set_value(float value) { return this->with_operation(NUMBER_OP_SET).with_value(value); } NumberCall &NumberCall::number_increment(bool cycle) { @@ -42,7 +53,7 @@ void NumberCall::perform() { const auto &traits = parent->traits; if (this->operation_ == NUMBER_OP_NONE) { - ESP_LOGW(TAG, "'%s' - NumberCall performed without selecting an operation", name); + this->log_perform_warning_(LOG_STR("No operation")); return; } @@ -51,28 +62,28 @@ void NumberCall::perform() { float max_value = traits.get_max_value(); if (this->operation_ == NUMBER_OP_SET) { - ESP_LOGD(TAG, "'%s' - Setting number value", name); + ESP_LOGD(TAG, "'%s': Setting value", name); if (!this->value_.has_value() || std::isnan(*this->value_)) { - ESP_LOGW(TAG, "'%s' - No value set for NumberCall", name); + this->log_perform_warning_(LOG_STR("No value")); return; } target_value = this->value_.value(); } else if (this->operation_ == NUMBER_OP_TO_MIN) { if (std::isnan(min_value)) { - ESP_LOGW(TAG, "'%s' - Can't set to min value through NumberCall: no min_value defined", name); + this->log_perform_warning_(LOG_STR("min undefined")); } else { target_value = min_value; } } else if (this->operation_ == NUMBER_OP_TO_MAX) { if (std::isnan(max_value)) { - ESP_LOGW(TAG, "'%s' - Can't set to max value through NumberCall: no max_value defined", name); + this->log_perform_warning_(LOG_STR("max undefined")); } else { target_value = max_value; } } else if (this->operation_ == NUMBER_OP_INCREMENT) { - ESP_LOGD(TAG, "'%s' - Increment number, with%s cycling", name, this->cycle_ ? "" : "out"); + ESP_LOGD(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? "" : "out"); if (!parent->has_state()) { - ESP_LOGW(TAG, "'%s' - Can't increment number through NumberCall: no active state to modify", name); + this->log_perform_warning_(LOG_STR("Can't increment, no state")); return; } auto step = traits.get_step(); @@ -85,9 +96,9 @@ void NumberCall::perform() { } } } else if (this->operation_ == NUMBER_OP_DECREMENT) { - ESP_LOGD(TAG, "'%s' - Decrement number, with%s cycling", name, this->cycle_ ? "" : "out"); + ESP_LOGD(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? "" : "out"); if (!parent->has_state()) { - ESP_LOGW(TAG, "'%s' - Can't decrement number through NumberCall: no active state to modify", name); + this->log_perform_warning_(LOG_STR("Can't decrement, no state")); return; } auto step = traits.get_step(); @@ -102,15 +113,15 @@ void NumberCall::perform() { } if (target_value < min_value) { - ESP_LOGW(TAG, "'%s' - Value %f must not be less than minimum %f", name, target_value, min_value); + this->log_perform_warning_value_range_(LOG_STR("<"), LOG_STR("min"), target_value, min_value); return; } if (target_value > max_value) { - ESP_LOGW(TAG, "'%s' - Value %f must not be greater than maximum %f", name, target_value, max_value); + this->log_perform_warning_value_range_(LOG_STR(">"), LOG_STR("max"), target_value, max_value); return; } - ESP_LOGD(TAG, " New number value: %f", target_value); + ESP_LOGD(TAG, " New value: %f", target_value); this->parent_->control(target_value); } diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h index bd50170be5..807207f0ec 100644 --- a/esphome/components/number/number_call.h +++ b/esphome/components/number/number_call.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #include "number_traits.h" namespace esphome { @@ -33,6 +34,10 @@ class NumberCall { NumberCall &with_cycle(bool cycle); protected: + void log_perform_warning_(const LogString *message); + void log_perform_warning_value_range_(const LogString *comparison, const LogString *limit_type, float val, + float limit); + Number *const parent_; NumberOperation operation_{NUMBER_OP_NONE}; optional value_; From 84c3cf5f1783ff12bd68fafd2a1b5aad9f72b903 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:38:58 -0500 Subject: [PATCH 41/99] [core] Replace std::pair with purpose-built named structs for component metadata (#10984) --- esphome/core/component.cpp | 46 +++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index ce4e2bf788..11d9501bb8 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -33,12 +33,22 @@ static const char *const TAG = "component"; // Using namespace-scope static to avoid guard variables (saves 16 bytes total) // This is safe because ESPHome is single-threaded during initialization namespace { +struct ComponentErrorMessage { + const Component *component; + const char *message; +}; + +struct ComponentPriorityOverride { + const Component *component; + float priority; +}; + // Error messages for failed components // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -std::unique_ptr>> component_error_messages; +std::unique_ptr> component_error_messages; // Setup priority overrides - freed after setup completes // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -std::unique_ptr>> setup_priority_overrides; +std::unique_ptr> setup_priority_overrides; } // namespace namespace setup_priority { @@ -134,9 +144,9 @@ void Component::call_dump_config() { // Look up error message from global vector const char *error_msg = nullptr; if (component_error_messages) { - for (const auto &pair : *component_error_messages) { - if (pair.first == this) { - error_msg = pair.second; + for (const auto &entry : *component_error_messages) { + if (entry.component == this) { + error_msg = entry.message; break; } } @@ -306,17 +316,17 @@ void Component::status_set_error(const char *message) { if (message != nullptr) { // Lazy allocate the error messages vector if needed if (!component_error_messages) { - component_error_messages = std::make_unique>>(); + component_error_messages = std::make_unique>(); } // Check if this component already has an error message - for (auto &pair : *component_error_messages) { - if (pair.first == this) { - pair.second = message; + for (auto &entry : *component_error_messages) { + if (entry.component == this) { + entry.message = message; return; } } // Add new error message - component_error_messages->emplace_back(this, message); + component_error_messages->emplace_back(ComponentErrorMessage{this, message}); } } void Component::status_clear_warning() { @@ -356,9 +366,9 @@ float Component::get_actual_setup_priority() const { // Check if there's an override in the global vector if (setup_priority_overrides) { // Linear search is fine for small n (typically < 5 overrides) - for (const auto &pair : *setup_priority_overrides) { - if (pair.first == this) { - return pair.second; + for (const auto &entry : *setup_priority_overrides) { + if (entry.component == this) { + return entry.priority; } } } @@ -367,21 +377,21 @@ float Component::get_actual_setup_priority() const { void Component::set_setup_priority(float priority) { // Lazy allocate the vector if needed if (!setup_priority_overrides) { - setup_priority_overrides = std::make_unique>>(); + setup_priority_overrides = std::make_unique>(); // Reserve some space to avoid reallocations (most configs have < 10 overrides) setup_priority_overrides->reserve(10); } // Check if this component already has an override - for (auto &pair : *setup_priority_overrides) { - if (pair.first == this) { - pair.second = priority; + for (auto &entry : *setup_priority_overrides) { + if (entry.component == this) { + entry.priority = priority; return; } } // Add new override - setup_priority_overrides->emplace_back(this, priority); + setup_priority_overrides->emplace_back(ComponentPriorityOverride{this, priority}); } bool Component::has_overridden_loop() const { From 5932a4bd0ee7ffa0225fdcf5bd6be059c4a4a08a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:42:23 -0500 Subject: [PATCH 42/99] [web_server] Reduce flash and RAM usage by optimizing string construction (#10986) --- esphome/components/web_server/web_server.cpp | 94 ++++++++++---------- esphome/core/helpers.cpp | 20 ++++- esphome/core/helpers.h | 5 ++ 3 files changed, 72 insertions(+), 47 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 33141c2049..95e0d13b58 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -381,11 +381,14 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { #endif // Helper functions to reduce code size by avoiding macro expansion -static void set_json_id(JsonObject &root, EntityBase *obj, const std::string &id, JsonDetail start_config) { - root["id"] = id; +static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) { + char id_buf[160]; // object_id can be up to 128 chars + prefix + dash + null + const auto &object_id = obj->get_object_id(); + snprintf(id_buf, sizeof(id_buf), "%s-%s", prefix, object_id.c_str()); + root["id"] = id_buf; if (start_config == DETAIL_ALL) { root["name"] = obj->get_name(); - root["icon"] = obj->get_icon(); + root["icon"] = obj->get_icon_ref(); root["entity_category"] = obj->get_entity_category(); bool is_disabled = obj->is_disabled_by_default(); if (is_disabled) @@ -393,17 +396,19 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const std::string &id } } +// Keep as separate function even though only used once: reduces code size by ~48 bytes +// by allowing compiler to share code between template instantiations (bool, float, etc.) template -static void set_json_value(JsonObject &root, EntityBase *obj, const std::string &id, const T &value, +static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix, const T &value, JsonDetail start_config) { - set_json_id(root, obj, id, start_config); + set_json_id(root, obj, prefix, start_config); root["value"] = value; } template -static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const std::string &id, - const std::string &state, const T &value, JsonDetail start_config) { - set_json_value(root, obj, id, value, start_config); +static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const std::string &state, + const T &value, JsonDetail start_config) { + set_json_value(root, obj, prefix, value, start_config); root["state"] = state; } @@ -442,20 +447,20 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail json::JsonBuilder builder; JsonObject root = builder.root(); + const auto uom_ref = obj->get_unit_of_measurement_ref(); + // Build JSON directly inline std::string state; if (std::isnan(value)) { state = "NA"; } else { - state = value_accuracy_to_string(value, obj->get_accuracy_decimals()); - if (!obj->get_unit_of_measurement().empty()) - state += " " + obj->get_unit_of_measurement(); + state = value_accuracy_with_uom_to_string(value, obj->get_accuracy_decimals(), uom_ref); } - set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config); + set_json_icon_state_value(root, obj, "sensor", state, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); - if (!obj->get_unit_of_measurement().empty()) - root["uom"] = obj->get_unit_of_measurement(); + if (!uom_ref.empty()) + root["uom"] = uom_ref; } return builder.serialize(); @@ -494,7 +499,7 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std: json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "text_sensor-" + obj->get_object_id(), value, value, start_config); + set_json_icon_state_value(root, obj, "text_sensor", value, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -567,7 +572,7 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); + set_json_icon_state_value(root, obj, "switch", value ? "ON" : "OFF", value, start_config); if (start_config == DETAIL_ALL) { root["assumed_state"] = obj->assumed_state(); this->add_sorting_info_(root, obj); @@ -607,7 +612,7 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "button-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "button", start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -647,8 +652,7 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value, - start_config); + set_json_icon_state_value(root, obj, "binary_sensor", value ? "ON" : "OFF", value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -717,8 +721,7 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state, - start_config); + set_json_icon_state_value(root, obj, "fan", obj->state ? "ON" : "OFF", obj->state, start_config); const auto traits = obj->get_traits(); if (traits.supports_speed()) { root["speed_level"] = obj->speed; @@ -793,7 +796,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "light-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "light", start_config); root["state"] = obj->remote_values.is_on() ? "ON" : "OFF"; light::LightJSONSchema::dump_json(*obj, root); @@ -881,8 +884,8 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", - obj->position, start_config); + set_json_icon_state_value(root, obj, "cover", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position, + start_config); root["current_operation"] = cover::cover_operation_to_str(obj->current_operation); if (obj->get_traits().get_supports_position()) @@ -939,7 +942,9 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "number-" + obj->get_object_id(), start_config); + const auto uom_ref = obj->traits.get_unit_of_measurement_ref(); + + set_json_id(root, obj, "number", start_config); if (start_config == DETAIL_ALL) { root["min_value"] = value_accuracy_to_string(obj->traits.get_min_value(), step_to_accuracy_decimals(obj->traits.get_step())); @@ -947,8 +952,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail value_accuracy_to_string(obj->traits.get_max_value(), step_to_accuracy_decimals(obj->traits.get_step())); root["step"] = value_accuracy_to_string(obj->traits.get_step(), step_to_accuracy_decimals(obj->traits.get_step())); root["mode"] = (int) obj->traits.get_mode(); - if (!obj->traits.get_unit_of_measurement().empty()) - root["uom"] = obj->traits.get_unit_of_measurement(); + if (!uom_ref.empty()) + root["uom"] = uom_ref; this->add_sorting_info_(root, obj); } if (std::isnan(value)) { @@ -956,10 +961,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail root["state"] = "NA"; } else { root["value"] = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); - std::string state = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); - if (!obj->traits.get_unit_of_measurement().empty()) - state += " " + obj->traits.get_unit_of_measurement(); - root["state"] = state; + root["state"] = + value_accuracy_with_uom_to_string(value, step_to_accuracy_decimals(obj->traits.get_step()), uom_ref); } return builder.serialize(); @@ -1013,7 +1016,7 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "date-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "date", start_config); std::string value = str_sprintf("%d-%02d-%02d", obj->year, obj->month, obj->day); root["value"] = value; root["state"] = value; @@ -1071,7 +1074,7 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "time-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "time", start_config); std::string value = str_sprintf("%02d:%02d:%02d", obj->hour, obj->minute, obj->second); root["value"] = value; root["state"] = value; @@ -1129,7 +1132,7 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "datetime-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "datetime", start_config); std::string value = str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, obj->minute, obj->second); root["value"] = value; @@ -1184,7 +1187,7 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "text-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "text", start_config); root["min_length"] = obj->traits.get_min_length(); root["max_length"] = obj->traits.get_max_length(); root["pattern"] = obj->traits.get_pattern(); @@ -1245,7 +1248,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); + set_json_icon_state_value(root, obj, "select", value, value, start_config); if (start_config == DETAIL_ALL) { JsonArray opt = root["option"].to(); for (auto &option : obj->traits.get_options()) { @@ -1314,7 +1317,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "climate", start_config); const auto traits = obj->get_traits(); int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); @@ -1467,8 +1470,7 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "lock-" + obj->get_object_id(), lock::lock_state_to_string(value), value, - start_config); + set_json_icon_state_value(root, obj, "lock", lock::lock_state_to_string(value), value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1546,8 +1548,8 @@ std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "valve-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", - obj->position, start_config); + set_json_icon_state_value(root, obj, "valve", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position, + start_config); root["current_operation"] = valve::valve_operation_to_str(obj->current_operation); if (obj->get_traits().get_supports_position()) @@ -1630,8 +1632,8 @@ std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmContro JsonObject root = builder.root(); char buf[16]; - 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); + set_json_icon_state_value(root, obj, "alarm-control-panel", PSTR_LOCAL(alarm_control_panel_state_to_string(value)), + value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1676,7 +1678,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "event-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "event", start_config); if (!event_type.empty()) { root["event_type"] = event_type; } @@ -1685,7 +1687,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty for (auto const &event_type : obj->get_event_types()) { event_types.add(event_type); } - root["device_class"] = obj->get_device_class(); + root["device_class"] = obj->get_device_class_ref(); this->add_sorting_info_(root, obj); } @@ -1748,7 +1750,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "update", start_config); root["value"] = obj->update_info.latest_version; root["state"] = update_state_to_string(obj->state); if (start_config == DETAIL_ALL) { diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 85c33ea2d3..d4f6809776 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -3,6 +3,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/string_ref.h" #include #include @@ -348,17 +349,34 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { return PARSE_NONE; } -std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { +static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_decimals) { if (accuracy_decimals < 0) { auto multiplier = powf(10.0f, accuracy_decimals); value = roundf(value * multiplier) / multiplier; accuracy_decimals = 0; } +} + +std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { + normalize_accuracy_decimals(value, accuracy_decimals); char tmp[32]; // should be enough, but we should maybe improve this at some point. snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); return std::string(tmp); } +std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement) { + normalize_accuracy_decimals(value, accuracy_decimals); + // Buffer sized for float (up to ~15 chars) + space + typical UOM (usually <20 chars like "μS/cm") + // snprintf truncates safely if exceeded, though ESPHome UOMs are typically short + char tmp[64]; + if (unit_of_measurement.empty()) { + snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); + } else { + snprintf(tmp, sizeof(tmp), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str()); + } + return std::string(tmp); +} + int8_t step_to_accuracy_decimals(float step) { // use printf %g to find number of digits based on temperature step char buf[32]; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 53e82abfef..e06f2d15ef 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -45,6 +45,9 @@ namespace esphome { +// Forward declaration to avoid circular dependency with string_ref.h +class StringRef; + /// @name STL backports ///@{ @@ -610,6 +613,8 @@ ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const ch /// Create a string from a value and an accuracy in decimals. std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); +/// Create a string from a value, an accuracy in decimals, and a unit of measurement. +std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement); /// Derive accuracy in decimals from an increment step. int8_t step_to_accuracy_decimals(float step); From 9b6d62cd691d1c27c2d71e1f3ed43595602ed0fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:55:39 -0500 Subject: [PATCH 43/99] [web_server_idf] Fix watchdog timeout with unreliable event source connections (#11002) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/web_server/web_server.cpp | 4 + .../web_server_idf/web_server_idf.cpp | 83 +++++++++++++++++-- .../web_server_idf/web_server_idf.h | 2 + 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 95e0d13b58..2df74e023e 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -127,6 +127,10 @@ void DeferredUpdateEventSource::process_deferred_queue_() { deferred_queue_.erase(deferred_queue_.begin()); this->consecutive_send_failures_ = 0; // Reset failure count on successful send } else { + // NOTE: Similar logic exists in web_server_idf/web_server_idf.cpp in AsyncEventSourceResponse::process_buffer_() + // The implementations differ due to platform-specific APIs (DISCARDED vs HTTPD_SOCK_ERR_TIMEOUT, close() vs + // fd_.store(0)), but the failure counting and timeout logic should be kept in sync. If you change this logic, + // also update the ESP-IDF implementation. this->consecutive_send_failures_++; if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) { // Too many failures, connection is likely dead diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 51d763c508..c04285402b 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -25,6 +25,10 @@ #include "esphome/components/web_server/list_entities.h" #endif // USE_WEBSERVER +// Include socket headers after Arduino headers to avoid IPADDR_NONE/INADDR_NONE macro conflicts +#include +#include + namespace esphome { namespace web_server_idf { @@ -46,6 +50,42 @@ DefaultHeaders default_headers_instance; DefaultHeaders &DefaultHeaders::Instance() { return default_headers_instance; } +namespace { +// Non-blocking send function to prevent watchdog timeouts when TCP buffers are full +/** + * Sends data on a socket in non-blocking mode. + * + * @param hd HTTP server handle (unused). + * @param sockfd Socket file descriptor. + * @param buf Buffer to send. + * @param buf_len Length of buffer. + * @param flags Flags for send(). + * @return + * - Number of bytes sent on success. + * - HTTPD_SOCK_ERR_INVALID if buf is nullptr. + * - HTTPD_SOCK_ERR_TIMEOUT if the send buffer is full (EAGAIN/EWOULDBLOCK). + * - HTTPD_SOCK_ERR_FAIL for other errors. + */ +int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) { + if (buf == nullptr) { + return HTTPD_SOCK_ERR_INVALID; + } + + // Use MSG_DONTWAIT to prevent blocking when TCP send buffer is full + int ret = send(sockfd, buf, buf_len, flags | MSG_DONTWAIT); + if (ret < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // Buffer full - retry later + return HTTPD_SOCK_ERR_TIMEOUT; + } + // Real error + ESP_LOGD(TAG, "send error: errno %d", errno); + return HTTPD_SOCK_ERR_FAIL; + } + return ret; +} +} // namespace + void AsyncWebServer::end() { if (this->server_) { httpd_stop(this->server_); @@ -384,6 +424,9 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * this->hd_ = req->handle; this->fd_.store(httpd_req_to_sockfd(req)); + // Use non-blocking send to prevent watchdog timeouts when TCP buffers are full + httpd_sess_set_send_override(this->hd_, this->fd_.load(), nonblocking_send); + // Configure reconnect timeout and send config // this should always go through since the tcp send buffer is empty on connect std::string message = ws->get_config_json(); @@ -459,15 +502,45 @@ void AsyncEventSourceResponse::process_buffer_() { return; } - 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 + size_t remaining = event_buffer_.size() - event_bytes_sent_; + int bytes_sent = + httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, remaining, 0); + if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT) { + // EAGAIN/EWOULDBLOCK - socket buffer full, try again later + // NOTE: Similar logic exists in web_server/web_server.cpp in DeferredUpdateEventSource::process_deferred_queue_() + // The implementations differ due to platform-specific APIs (HTTPD_SOCK_ERR_TIMEOUT vs DISCARDED, fd_.store(0) vs + // close()), but the failure counting and timeout logic should be kept in sync. If you change this logic, also + // update the Arduino implementation. + 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->fd_.store(0); // Mark for cleanup + this->deferred_queue_.clear(); + } return; } + if (bytes_sent == HTTPD_SOCK_ERR_FAIL) { + // Real socket error - connection will be closed by httpd and destroy callback will be called + return; + } + if (bytes_sent <= 0) { + // Unexpected error or zero bytes sent + ESP_LOGW(TAG, "Unexpected send result: %d", bytes_sent); + return; + } + + // Successful send - reset failure counter + this->consecutive_send_failures_ = 0; event_bytes_sent_ += bytes_sent; + // Log partial sends for debugging + if (event_bytes_sent_ < event_buffer_.size()) { + ESP_LOGV(TAG, "Partial send: %d/%zu bytes (total: %zu/%zu)", bytes_sent, remaining, event_bytes_sent_, + event_buffer_.size()); + } + if (event_bytes_sent_ == event_buffer_.size()) { event_buffer_.resize(0); event_bytes_sent_ = 0; diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 76540ef232..64fda12fda 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -283,6 +283,8 @@ class AsyncEventSourceResponse { std::unique_ptr entities_iterator_; std::string event_buffer_{""}; size_t event_bytes_sent_; + uint16_t consecutive_send_failures_{0}; + static constexpr uint16_t MAX_CONSECUTIVE_SEND_FAILURES = 2500; // ~20 seconds at 125Hz loop rate }; using AsyncEventSourceClient = AsyncEventSourceResponse; From c0fb0ae06ff076e1c1de3ee8a1d21ea5f34a7f39 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:57:59 -0500 Subject: [PATCH 44/99] [web_server_idf] Optimize parameter storage to reduce flash usage and memory overhead (#11003) --- .../web_server_idf/web_server_idf.cpp | 44 +++++++++++++------ .../web_server_idf/web_server_idf.h | 10 ++++- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index c04285402b..60a1b8acbf 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -204,8 +204,8 @@ esp_err_t AsyncWebServer::request_handler_(AsyncWebServerRequest *request) const AsyncWebServerRequest::~AsyncWebServerRequest() { delete this->rsp_; - for (const auto &pair : this->params_) { - delete pair.second; // NOLINT(cppcoreguidelines-owning-memory) + for (auto *param : this->params_) { + delete param; // NOLINT(cppcoreguidelines-owning-memory) } } @@ -245,10 +245,22 @@ void AsyncWebServerRequest::redirect(const std::string &url) { } void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type) { - httpd_resp_set_status(*this, code == 200 ? HTTPD_200 - : code == 404 ? HTTPD_404 - : code == 409 ? HTTPD_409 - : to_string(code).c_str()); + // Set status code - use constants for common codes to avoid string allocation + const char *status = nullptr; + switch (code) { + case 200: + status = HTTPD_200; + break; + case 404: + status = HTTPD_404; + break; + case 409: + status = HTTPD_409; + break; + default: + break; + } + httpd_resp_set_status(*this, status == nullptr ? to_string(code).c_str() : status); if (content_type && *content_type) { httpd_resp_set_type(*this, content_type); @@ -305,11 +317,14 @@ void AsyncWebServerRequest::requestAuthentication(const char *realm) const { #endif AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { - auto find = this->params_.find(name); - if (find != this->params_.end()) { - return find->second; + // Check cache first - only successful lookups are cached + for (auto *param : this->params_) { + if (param->name() == name) { + return param; + } } + // Look up value from query strings optional val = query_key_value(this->post_query_, name); if (!val.has_value()) { auto url_query = request_get_url_query(*this); @@ -318,11 +333,14 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { } } - AsyncWebParameter *param = nullptr; - if (val.has_value()) { - param = new AsyncWebParameter(val.value()); // NOLINT(cppcoreguidelines-owning-memory) + // Don't cache misses to avoid wasting memory when handlers check for + // optional parameters that don't exist in the request + if (!val.has_value()) { + return nullptr; } - this->params_.insert({name, param}); + + auto *param = new AsyncWebParameter(name, val.value()); // NOLINT(cppcoreguidelines-owning-memory) + this->params_.push_back(param); return param; } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 64fda12fda..3d482da0a5 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -30,10 +30,12 @@ using String = std::string; class AsyncWebParameter { public: - AsyncWebParameter(std::string value) : value_(std::move(value)) {} + AsyncWebParameter(std::string name, std::string value) : name_(std::move(name)), value_(std::move(value)) {} + const std::string &name() const { return this->name_; } const std::string &value() const { return this->value_; } protected: + std::string name_; std::string value_; }; @@ -174,7 +176,11 @@ class AsyncWebServerRequest { protected: httpd_req_t *req_; AsyncWebServerResponse *rsp_{}; - std::map params_; + // Use vector instead of map/unordered_map: most requests have 0-3 params, so linear search + // is faster than tree/hash overhead. AsyncWebParameter stores both name and value to avoid + // duplicate storage. Only successful lookups are cached to prevent cache pollution when + // handlers check for optional parameters that don't exist. + std::vector params_; std::string post_query_; AsyncWebServerRequest(httpd_req_t *req) : req_(req) {} AsyncWebServerRequest(httpd_req_t *req, std::string post_query) : req_(req), post_query_(std::move(post_query)) {} From 20d9ae699c18433a397c9962bbc257eac27154a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 15:59:52 -0500 Subject: [PATCH 45/99] [logger] Conditionally compile runtime tag-specific log levels for performance (#11004) --- esphome/components/logger/__init__.py | 11 +++++++++-- esphome/components/logger/logger.cpp | 10 ++++++++-- esphome/components/logger/logger.h | 15 +++++++++++++-- esphome/core/defines.h | 1 + tests/components/logger/common-default_uart.yaml | 5 +++++ 5 files changed, 36 insertions(+), 6 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 7d1a591f0c..1d02073d27 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -95,6 +95,7 @@ DEFAULT = "DEFAULT" CONF_INITIAL_LEVEL = "initial_level" CONF_LOGGER_ID = "logger_id" +CONF_RUNTIME_TAG_LEVELS = "runtime_tag_levels" CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size" UART_SELECTION_ESP32 = { @@ -249,6 +250,7 @@ CONFIG_SCHEMA = cv.All( } ), cv.Optional(CONF_INITIAL_LEVEL): is_log_level, + cv.Optional(CONF_RUNTIME_TAG_LEVELS, default=False): cv.boolean, cv.Optional(CONF_ON_MESSAGE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger), @@ -291,8 +293,12 @@ async def to_code(config): ) cg.add(log.pre_setup()) - for tag, log_level in config[CONF_LOGS].items(): - cg.add(log.set_log_level(tag, LOG_LEVELS[log_level])) + # Enable runtime tag levels if logs are configured or explicitly enabled + logs_config = config[CONF_LOGS] + if logs_config or config[CONF_RUNTIME_TAG_LEVELS]: + cg.add_define("USE_LOGGER_RUNTIME_TAG_LEVELS") + for tag, log_level in logs_config.items(): + cg.add(log.set_log_level(tag, LOG_LEVELS[log_level])) cg.add_define("USE_LOGGER") this_severity = LOG_LEVEL_SEVERITY.index(level) @@ -443,6 +449,7 @@ async def logger_set_level_to_code(config, action_id, template_arg, args): level = LOG_LEVELS[config[CONF_LEVEL]] logger = await cg.get_variable(config[CONF_LOGGER_ID]) if tag := config.get(CONF_TAG): + cg.add_define("USE_LOGGER_RUNTIME_TAG_LEVELS") text = str(cg.statement(logger.set_log_level(tag, level))) else: text = str(cg.statement(logger.set_log_level(level))) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 4a69bd9853..9a9bf89fe3 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -148,9 +148,11 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas #endif // USE_STORE_LOG_STR_IN_FLASH inline uint8_t Logger::level_for(const char *tag) { +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS auto it = this->log_levels_.find(tag); if (it != this->log_levels_.end()) return it->second; +#endif return this->current_level_; } @@ -220,7 +222,9 @@ void Logger::process_messages_() { } void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } -void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS +void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } +#endif #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) UARTSelection Logger::get_uart() const { return this->uart_; } @@ -271,9 +275,11 @@ void Logger::dump_config() { } #endif +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS for (auto &it : this->log_levels_) { - ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first.c_str(), LOG_STR_ARG(LOG_LEVELS[it.second])); + ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(LOG_LEVELS[it.second])); } +#endif } void Logger::set_log_level(uint8_t level) { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index f0e0ed9a27..2099520049 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -36,6 +36,13 @@ struct device; namespace esphome::logger { +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS +// Comparison function for const char* keys in log_levels_ map +struct CStrCompare { + bool operator()(const char *a, const char *b) const { return strcmp(a, b) < 0; } +}; +#endif + // ANSI color code last digit (30-38 range, store only last digit to save RAM) static constexpr char LOG_LEVEL_COLOR_DIGIT[] = { '\0', // NONE @@ -133,8 +140,10 @@ class Logger : public Component { /// Set the default log level for this logger. void set_log_level(uint8_t level); +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS /// Set the log level of the specified tag. - void set_log_level(const std::string &tag, uint8_t log_level); + void set_log_level(const char *tag, uint8_t log_level); +#endif uint8_t get_log_level() { return this->current_level_; } // ========== INTERNAL METHODS ========== @@ -242,7 +251,9 @@ class Logger : public Component { #endif // Large objects (internally aligned) - std::map log_levels_{}; +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS + std::map log_levels_{}; +#endif CallbackManager log_callback_{}; CallbackManager level_callback_{}; #ifdef USE_ESPHOME_TASK_LOG_BUFFER diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 554e1ee13c..d560007e71 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -48,6 +48,7 @@ #define USE_LIGHT #define USE_LOCK #define USE_LOGGER +#define USE_LOGGER_RUNTIME_TAG_LEVELS #define USE_LVGL #define USE_LVGL_ANIMIMG #define USE_LVGL_ARC diff --git a/tests/components/logger/common-default_uart.yaml b/tests/components/logger/common-default_uart.yaml index e8b56043eb..7939a5f9c5 100644 --- a/tests/components/logger/common-default_uart.yaml +++ b/tests/components/logger/common-default_uart.yaml @@ -6,11 +6,16 @@ esphome: format: "Warning: Logger level is %d" args: [id(logger_id).get_log_level()] - logger.set_level: WARN + - logger.set_level: + level: ERROR + tag: mqtt.client logger: id: logger_id level: DEBUG initial_level: INFO + logs: + mqtt.component: WARN select: - platform: logger From aa1afbd152cda35f8ab5b5dfda7839eaf68c47b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 16:02:41 -0500 Subject: [PATCH 46/99] [wifi] Optimize WPA2 EAP phase2 logging to reduce memory overhead (#11005) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/wifi/wifi_component.cpp | 31 +++++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8c7b55c274..c86e233059 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1,7 +1,6 @@ #include "wifi_component.h" #ifdef USE_WIFI #include -#include #ifdef USE_ESP32 #if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1) @@ -42,6 +41,25 @@ namespace wifi { static const char *const TAG = "wifi"; +#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE +static const char *eap_phase2_to_str(esp_eap_ttls_phase2_types type) { + switch (type) { + case ESP_EAP_TTLS_PHASE2_PAP: + return "pap"; + case ESP_EAP_TTLS_PHASE2_CHAP: + return "chap"; + case ESP_EAP_TTLS_PHASE2_MSCHAP: + return "mschap"; + case ESP_EAP_TTLS_PHASE2_MSCHAPV2: + return "mschapv2"; + case ESP_EAP_TTLS_PHASE2_EAP: + return "eap"; + default: + return "unknown"; + } +} +#endif + float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; } void WiFiComponent::setup() { @@ -344,15 +362,8 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str()); ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str()); ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str()); -#ifdef USE_ESP32 -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - std::map phase2types = {{ESP_EAP_TTLS_PHASE2_PAP, "pap"}, - {ESP_EAP_TTLS_PHASE2_CHAP, "chap"}, - {ESP_EAP_TTLS_PHASE2_MSCHAP, "mschap"}, - {ESP_EAP_TTLS_PHASE2_MSCHAPV2, "mschapv2"}, - {ESP_EAP_TTLS_PHASE2_EAP, "eap"}}; - ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), phase2types[eap_config.ttls_phase_2].c_str()); -#endif +#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), eap_phase2_to_str(eap_config.ttls_phase_2)); #endif bool ca_cert_present = eap_config.ca_cert != nullptr && strlen(eap_config.ca_cert); bool client_cert_present = eap_config.client_cert != nullptr && strlen(eap_config.client_cert); From 7ea51b1865eb2f85abcd1dcdd3345d548655924d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 16:17:28 -0500 Subject: [PATCH 47/99] [esphome.ota] Fix ESP32-S3 OTA authentication with hardware SHA acceleration (#11011) --- .../components/esphome/ota/ota_esphome.cpp | 156 +++++++++--------- esphome/components/esphome/ota/ota_esphome.h | 2 - esphome/components/sha256/sha256.cpp | 33 ++++ esphome/components/sha256/sha256.h | 4 + esphome/core/hash_base.h | 2 +- 5 files changed, 119 insertions(+), 78 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index f1506f066c..b65bfc5ab8 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -614,24 +614,67 @@ bool ESPHomeOTAComponent::handle_auth_send_() { return false; } - // Generate nonce with appropriate hasher - bool success = false; + // Generate nonce - hasher must be created and used in same stack frame + // CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS: + // 1. Hash objects must NEVER be passed to another function (different stack frame) + // 2. NO Variable Length Arrays (VLAs) - they corrupt the stack with hardware DMA + // 3. All hash operations (init/add/calculate) must happen in the SAME function where object is created + // Violating these causes truncated hash output (20 bytes instead of 32) or memory corruption. + // + // Buffer layout after AUTH_READ completes: + // [0]: auth_type (1 byte) + // [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND + // [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce + // [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash + + // Declare both hash objects in same stack frame, use pointer to select. + // NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3 + // hardware SHA acceleration - the object must exist in this stack frame for all operations. + // Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope. +#ifdef USE_OTA_SHA256 + sha256::SHA256 sha_hasher; +#endif +#ifdef USE_OTA_MD5 + md5::MD5Digest md5_hasher; +#endif + HashBase *hasher = nullptr; + #ifdef USE_OTA_SHA256 if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { - sha256::SHA256 sha_hasher; - success = this->prepare_auth_nonce_(&sha_hasher); + hasher = &sha_hasher; } #endif #ifdef USE_OTA_MD5 if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { - md5::MD5Digest md5_hasher; - success = this->prepare_auth_nonce_(&md5_hasher); + hasher = &md5_hasher; } #endif - if (!success) { + const size_t hex_size = hasher->get_size() * 2; + const size_t nonce_len = hasher->get_size() / 4; + const size_t auth_buf_size = 1 + 3 * hex_size; + this->auth_buf_ = std::make_unique(auth_buf_size); + this->auth_buf_pos_ = 0; + + char *buf = reinterpret_cast(this->auth_buf_.get() + 1); + if (!random_bytes(reinterpret_cast(buf), nonce_len)) { + this->log_auth_warning_(LOG_STR("Random failed")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN); return false; } + + hasher->init(); + hasher->add(buf, nonce_len); + hasher->calculate(); + this->auth_buf_[0] = this->auth_type_; + hasher->get_hex(buf); + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too + memcpy(log_buf, buf, hex_size); + log_buf[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf); +#endif } // Try to write auth_type + nonce @@ -678,89 +721,41 @@ bool ESPHomeOTAComponent::handle_auth_read_() { } // We have all the data, verify it - bool matches = false; + const char *nonce = reinterpret_cast(this->auth_buf_.get() + 1); + const char *cnonce = nonce + hex_size; + const char *response = cnonce + hex_size; + + // CRITICAL ESP32-S3: Hash objects must stay in same stack frame (no passing to other functions). + // Declare both hash objects in same stack frame, use pointer to select. + // NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3 + // hardware SHA acceleration - the object must exist in this stack frame for all operations. + // Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope. +#ifdef USE_OTA_SHA256 + sha256::SHA256 sha_hasher; +#endif +#ifdef USE_OTA_MD5 + md5::MD5Digest md5_hasher; +#endif + HashBase *hasher = nullptr; #ifdef USE_OTA_SHA256 if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { - sha256::SHA256 sha_hasher; - matches = this->verify_hash_auth_(&sha_hasher, hex_size); + hasher = &sha_hasher; } #endif #ifdef USE_OTA_MD5 if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { - md5::MD5Digest md5_hasher; - matches = this->verify_hash_auth_(&md5_hasher, hex_size); + hasher = &md5_hasher; } #endif - if (!matches) { - this->log_auth_warning_(LOG_STR("Password mismatch")); - this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); - return false; - } - - // Authentication successful - clean up auth state - this->cleanup_auth_(); - - return true; -} - -bool ESPHomeOTAComponent::prepare_auth_nonce_(HashBase *hasher) { - // Calculate required buffer size using the hasher - const size_t hex_size = hasher->get_size() * 2; - const size_t nonce_len = hasher->get_size() / 4; - - // Buffer layout after AUTH_READ completes: - // [0]: auth_type (1 byte) - // [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND - // [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce - // [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash - // Total: 1 + 3*hex_size - const size_t auth_buf_size = 1 + 3 * hex_size; - this->auth_buf_ = std::make_unique(auth_buf_size); - this->auth_buf_pos_ = 0; - - // Generate nonce - char *buf = reinterpret_cast(this->auth_buf_.get() + 1); - if (!random_bytes(reinterpret_cast(buf), nonce_len)) { - this->log_auth_warning_(LOG_STR("Random failed")); - this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN); - return false; - } - - hasher->init(); - hasher->add(buf, nonce_len); - hasher->calculate(); - - // Prepare buffer: auth_type (1 byte) + nonce (hex_size bytes) - this->auth_buf_[0] = this->auth_type_; - hasher->get_hex(buf); - -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - char log_buf[hex_size + 1]; - // Log nonce for debugging - memcpy(log_buf, buf, hex_size); - log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf); -#endif - - return true; -} - -bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) { - // Get pointers to the data in the buffer (see prepare_auth_nonce_ for buffer layout) - const char *nonce = reinterpret_cast(this->auth_buf_.get() + 1); // Skip auth_type byte - const char *cnonce = nonce + hex_size; // CNonce immediately follows nonce - const char *response = cnonce + hex_size; // Response immediately follows cnonce - - // Calculate expected hash: password + nonce + cnonce hasher->init(); hasher->add(this->password_.c_str(), this->password_.length()); hasher->add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer) hasher->calculate(); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - char log_buf[hex_size + 1]; + char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too // Log CNonce memcpy(log_buf, cnonce, hex_size); log_buf[hex_size] = '\0'; @@ -778,7 +773,18 @@ bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) { #endif // Compare response - return hasher->equals_hex(response); + bool matches = hasher->equals_hex(response); + + if (!matches) { + this->log_auth_warning_(LOG_STR("Password mismatch")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + return false; + } + + // Authentication successful - clean up auth state + this->cleanup_auth_(); + + return true; } size_t ESPHomeOTAComponent::get_auth_hex_size_() const { diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 1e26494fd0..d4a8410d35 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -47,8 +47,6 @@ class ESPHomeOTAComponent : public ota::OTAComponent { bool handle_auth_send_(); bool handle_auth_read_(); bool select_auth_type_(); - bool prepare_auth_nonce_(HashBase *hasher); - bool verify_hash_auth_(HashBase *hasher, size_t hex_size); size_t get_auth_hex_size_() const; void cleanup_auth_(); void log_auth_warning_(const LogString *msg); diff --git a/esphome/components/sha256/sha256.cpp b/esphome/components/sha256/sha256.cpp index 199460acbc..32abbd739d 100644 --- a/esphome/components/sha256/sha256.cpp +++ b/esphome/components/sha256/sha256.cpp @@ -10,6 +10,39 @@ namespace esphome::sha256 { #if defined(USE_ESP32) || defined(USE_LIBRETINY) +// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS: +// +// The ESP32-S3 uses hardware DMA for SHA acceleration. The mbedtls_sha256_context structure contains +// internal state that the DMA engine references. This imposes two critical constraints: +// +// 1. NO VARIABLE LENGTH ARRAYS (VLAs): VLAs corrupt the stack layout, causing the DMA engine to +// write to incorrect memory locations. This results in null pointer dereferences and crashes. +// ALWAYS use fixed-size arrays (e.g., char buf[65], not char buf[size+1]). +// +// 2. SAME STACK FRAME ONLY: The SHA256 object must be created and used entirely within the same +// function. NEVER pass the SHA256 object or HashBase pointer to another function. When the stack +// frame changes (function call/return), the DMA references become invalid and will produce +// truncated hash output (20 bytes instead of 32) or corrupt memory. +// +// CORRECT USAGE: +// void my_function() { +// sha256::SHA256 hasher; // Created locally +// hasher.init(); +// hasher.add(data, len); // Any size, no chunking needed +// hasher.calculate(); +// bool ok = hasher.equals_hex(expected); +// // hasher destroyed when function returns +// } +// +// INCORRECT USAGE (WILL FAIL ON ESP32-S3): +// void my_function() { +// sha256::SHA256 hasher; +// helper(&hasher); // WRONG: Passed to different stack frame +// } +// void helper(HashBase *h) { +// h->init(); // WRONG: Will produce truncated/corrupted output +// } + SHA256::~SHA256() { mbedtls_sha256_free(&this->ctx_); } void SHA256::init() { diff --git a/esphome/components/sha256/sha256.h b/esphome/components/sha256/sha256.h index bb089bc314..a2b62799e1 100644 --- a/esphome/components/sha256/sha256.h +++ b/esphome/components/sha256/sha256.h @@ -39,6 +39,10 @@ class SHA256 : public esphome::HashBase { protected: #if defined(USE_ESP32) || defined(USE_LIBRETINY) + // CRITICAL: The mbedtls context MUST be stack-allocated (not a pointer) for ESP32-S3 hardware SHA acceleration. + // The ESP32-S3 DMA engine references this structure's memory addresses. If the context is passed to another + // function (crossing stack frames) or if VLAs are present, the DMA operations will corrupt memory and produce + // truncated/incorrect hash results. mbedtls_sha256_context ctx_{}; #elif defined(USE_ESP8266) || defined(USE_RP2040) br_sha256_context ctx_{}; diff --git a/esphome/core/hash_base.h b/esphome/core/hash_base.h index 4eb6a89f53..c45c4df70b 100644 --- a/esphome/core/hash_base.h +++ b/esphome/core/hash_base.h @@ -39,7 +39,7 @@ class HashBase { /// Compare the hash against a provided hex-encoded hash bool equals_hex(const char *expected) { - uint8_t parsed[this->get_size()]; + uint8_t parsed[32]; // Fixed size for max hash (SHA256 = 32 bytes) if (!parse_hex(expected, parsed, this->get_size())) { return false; } From e2c5eeef975fea9f814a191c2cc10fbe35698f68 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 16:32:51 -0500 Subject: [PATCH 48/99] [scheduler] Deduplicate item removal code with template helper (#11017) --- esphome/core/scheduler.cpp | 26 +++++--------------------- esphome/core/scheduler.h | 27 +++++++++++++++++++-------- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 71e2a00fbe..402084f306 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -118,7 +118,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->type = type; item->callback = std::move(func); // Initialize remove to false (though it should already be from constructor) - // Not using mark_item_removed_ helper since we're setting to false, not true #ifdef ESPHOME_THREAD_MULTI_ATOMICS item->remove.store(false, std::memory_order_relaxed); #else @@ -600,12 +599,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c #ifndef ESPHOME_THREAD_SINGLE // Mark items in defer queue as cancelled (they'll be skipped when processed) if (type == SchedulerItem::TIMEOUT) { - for (auto &item : this->defer_queue_) { - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { - this->mark_item_removed_(item.get()); - total_cancelled++; - } - } + total_cancelled += this->mark_matching_items_removed_(this->defer_queue_, component, name_cstr, type, match_retry); } #endif /* not ESPHOME_THREAD_SINGLE */ @@ -620,23 +614,13 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c total_cancelled++; } // For other items in heap, we can only mark for removal (can't remove from middle of heap) - for (auto &item : this->items_) { - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { - this->mark_item_removed_(item.get()); - total_cancelled++; - this->to_remove_++; // Track removals for heap items - } - } + size_t heap_cancelled = this->mark_matching_items_removed_(this->items_, component, name_cstr, type, match_retry); + total_cancelled += heap_cancelled; + this->to_remove_ += heap_cancelled; // 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, match_retry)) { - this->mark_item_removed_(item.get()); - total_cancelled++; - // Don't track removals for to_add_ items - } - } + total_cancelled += this->mark_matching_items_removed_(this->to_add_, component, name_cstr, type, match_retry); return total_cancelled > 0; } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 885ee13754..2237915e07 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -280,19 +280,30 @@ class Scheduler { #endif } - // Helper to mark item for removal (platform-specific) + // Helper to mark matching items in a container as removed + // Returns the number of items marked for removal // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this // function. - void mark_item_removed_(SchedulerItem *item) { + template + size_t mark_matching_items_removed_(Container &container, Component *component, const char *name_cstr, + SchedulerItem::Type type, bool match_retry) { + size_t count = 0; + for (auto &item : container) { + if (this->matches_item_(item, component, name_cstr, type, match_retry)) { + // Mark item for removal (platform-specific) #ifdef ESPHOME_THREAD_MULTI_ATOMICS - // Multi-threaded with atomics: use atomic store - item->remove.store(true, std::memory_order_release); + // Multi-threaded with atomics: use atomic store + item->remove.store(true, std::memory_order_release); #else - // Single-threaded (ESPHOME_THREAD_SINGLE) or - // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write - // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock! - item->remove = true; + // Single-threaded (ESPHOME_THREAD_SINGLE) or + // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write + // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock! + item->remove = true; #endif + count++; + } + } + return count; } // Template helper to check if any item in a container matches our criteria From c6e4a7911c110711c89fe8f1eb035a442b8919aa Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 5 Oct 2025 18:10:23 -0400 Subject: [PATCH 49/99] [esp32] Improve version handling (#10899) Co-authored-by: J. Nick Koston --- esphome/components/esp32/__init__.py | 248 ++++++++++----------------- script/generate-esp32-boards.py | 3 +- 2 files changed, 93 insertions(+), 158 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f5eda52cae..3fbbf68c71 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -296,14 +296,9 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip" -def _format_framework_espidf_version( - ver: cv.Version, release: str, for_platformio: bool -) -> str: - # format the given arduino (https://github.com/espressif/esp-idf/releases) version to +def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: + # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to # a PIO platformio/framework-espidf value - # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf - if for_platformio: - return f"platformio/framework-espidf@~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" if release: return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip" return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip" @@ -317,157 +312,108 @@ def _format_framework_espidf_version( # The default/recommended arduino framework version # - https://github.com/espressif/arduino-esp32/releases -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1) -# The platform-espressif32 version to use for arduino frameworks -# - https://github.com/pioarduino/platform-espressif32/releases -ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21, "2") +ARDUINO_FRAMEWORK_VERSION_LOOKUP = { + "recommended": cv.Version(3, 2, 1), + "latest": cv.Version(3, 3, 1), + "dev": cv.Version(3, 3, 1), +} +ARDUINO_PLATFORM_VERSION_LOOKUP = { + cv.Version(3, 3, 1): cv.Version(55, 3, 31), + cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"), + cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"), + cv.Version(3, 2, 0): cv.Version(54, 3, 20), + cv.Version(3, 1, 3): cv.Version(53, 3, 13), + cv.Version(3, 1, 2): cv.Version(53, 3, 12), + cv.Version(3, 1, 1): cv.Version(53, 3, 11), + cv.Version(3, 1, 0): cv.Version(53, 3, 10), +} # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases -# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf -RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2) -# The platformio/espressif32 version to use for esp-idf frameworks -# - https://github.com/platformio/platform-espressif32/releases -# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 -ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21, "2") +ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { + "recommended": cv.Version(5, 4, 2), + "latest": cv.Version(5, 5, 1), + "dev": cv.Version(5, 5, 1), +} +ESP_IDF_PLATFORM_VERSION_LOOKUP = { + cv.Version(5, 5, 1): cv.Version(55, 3, 31), + cv.Version(5, 5, 0): cv.Version(55, 3, 31), + cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"), + cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"), + cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"), + cv.Version(5, 3, 2): cv.Version(53, 3, 13), + cv.Version(5, 3, 1): cv.Version(53, 3, 13), + cv.Version(5, 3, 0): cv.Version(53, 3, 13), + cv.Version(5, 1, 6): cv.Version(51, 3, 7), + cv.Version(5, 1, 5): cv.Version(51, 3, 7), +} -# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions -SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ - cv.Version(5, 3, 1), - cv.Version(5, 3, 0), - cv.Version(5, 2, 2), - cv.Version(5, 2, 1), - cv.Version(5, 1, 2), - cv.Version(5, 1, 1), - cv.Version(5, 1, 0), - cv.Version(5, 0, 2), - cv.Version(5, 0, 1), - cv.Version(5, 0, 0), -] - -# pioarduino versions that don't require a release number -# List based on https://github.com/pioarduino/esp-idf/releases -SUPPORTED_PIOARDUINO_ESP_IDF_5X = [ - cv.Version(5, 5, 1), - 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), - cv.Version(5, 3, 2), - cv.Version(5, 3, 1), - cv.Version(5, 3, 0), - cv.Version(5, 1, 5), - cv.Version(5, 1, 6), -] +# The platform-espressif32 version +# - https://github.com/pioarduino/platform-espressif32/releases +PLATFORM_VERSION_LOOKUP = { + "recommended": cv.Version(54, 3, 21, "2"), + "latest": cv.Version(55, 3, 31), + "dev": "https://github.com/pioarduino/platform-espressif32.git#develop", +} def _check_versions(value): value = value.copy() - if value[CONF_TYPE] == FRAMEWORK_ARDUINO: - lookups = { - "dev": ( - cv.Version(3, 2, 1), - "https://github.com/espressif/arduino-esp32.git", - ), - "latest": (cv.Version(3, 2, 1), None), - "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), - } - if value[CONF_VERSION] in lookups: - if CONF_SOURCE in value: - raise cv.Invalid( - "Framework version needs to be explicitly specified when custom source is used." - ) - - version, source = lookups[value[CONF_VERSION]] - else: - version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) - source = value.get(CONF_SOURCE, None) - - value[CONF_VERSION] = str(version) - value[CONF_SOURCE] = source or _format_framework_arduino_version(version) - - value[CONF_PLATFORM_VERSION] = value.get( - 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. " - "If there are connectivity or build issues please remove the manual version." - ) - - return value - - lookups = { - "dev": (cv.Version(5, 4, 2), "https://github.com/espressif/esp-idf.git"), - "latest": (cv.Version(5, 2, 2), None), - "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), - } - - if value[CONF_VERSION] in lookups: - if CONF_SOURCE in value: + if value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP: + if CONF_SOURCE in value or CONF_PLATFORM_VERSION in value: raise cv.Invalid( - "Framework version needs to be explicitly specified when custom source is used." + "Version needs to be explicitly set when a custom source or platform_version is used." ) - version, source = lookups[value[CONF_VERSION]] + platform_lookup = PLATFORM_VERSION_LOOKUP[value[CONF_VERSION]] + value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup)) + + if value[CONF_TYPE] == FRAMEWORK_ARDUINO: + version = ARDUINO_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]] + else: + version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]] else: version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) - source = value.get(CONF_SOURCE, None) - - 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 - - value[CONF_PLATFORM_VERSION] = value.get( - CONF_PLATFORM_VERSION, _parse_platform_version(str(ESP_IDF_PLATFORM_VERSION)) - ) - - if ( - 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 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}'" - ) - - if ( - not is_platformio - and CONF_RELEASE not in value - and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X - ): - raise cv.Invalid( - f"ESP-IDF {value[CONF_VERSION]} is not available with pioarduino; you may need to specify '{CONF_RELEASE}'" - ) value[CONF_VERSION] = str(version) - value[CONF_SOURCE] = source or _format_framework_espidf_version( - version, value.get(CONF_RELEASE, None), is_platformio - ) - if value[CONF_SOURCE].startswith("http"): - # prefix is necessary or platformio will complain with a cryptic error - value[CONF_SOURCE] = f"framework-espidf@{value[CONF_SOURCE]}" + if value[CONF_TYPE] == FRAMEWORK_ARDUINO: + if version < cv.Version(3, 0, 0): + raise cv.Invalid("Only Arduino 3.0+ is supported.") + recommended_version = ARDUINO_FRAMEWORK_VERSION_LOOKUP["recommended"] + platform_lookup = ARDUINO_PLATFORM_VERSION_LOOKUP.get(version) + value[CONF_SOURCE] = value.get( + CONF_SOURCE, _format_framework_arduino_version(version) + ) + else: + if version < cv.Version(5, 0, 0): + raise cv.Invalid("Only ESP-IDF 5.0+ is supported.") + recommended_version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP["recommended"] + platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version) + value[CONF_SOURCE] = value.get( + CONF_SOURCE, + _format_framework_espidf_version(version, value.get(CONF_RELEASE, None)), + ) - if version != RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION: + if CONF_PLATFORM_VERSION not in value: + if platform_lookup is None: + raise cv.Invalid( + "Framework version not recognized; please specify platform_version" + ) + value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup)) + + if version != recommended_version: _LOGGER.warning( - "The selected ESP-IDF framework version is not the recommended one. " + "The selected framework version is not the recommended one. " + "If there are connectivity or build issues please remove the manual version." + ) + + if value[CONF_PLATFORM_VERSION] != _parse_platform_version( + str(PLATFORM_VERSION_LOOKUP["recommended"]) + ): + _LOGGER.warning( + "The selected platform version is not the recommended one. " "If there are connectivity or build issues please remove the manual version." ) @@ -477,26 +423,14 @@ def _check_versions(value): def _parse_platform_version(value): try: ver = cv.Version.parse(cv.version_number(value)) - if ver.major >= 50: # a pioarduino version - release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" - if ver.extra: - release += f"-{ver.extra}" - return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip" - # if platform version is a valid version constraint, prefix the default package - cv.platformio_version_constraint(value) - return f"platformio/espressif32@{value}" + release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" + if ver.extra: + release += f"-{ver.extra}" + return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip" except cv.Invalid: return value -def _platform_is_platformio(value): - try: - ver = cv.Version.parse(cv.version_number(value)) - return ver.major < 50 - except cv.Invalid: - return "platformio" in value - - def _detect_variant(value): board = value.get(CONF_BOARD) variant = value.get(CONF_VARIANT) @@ -808,6 +742,8 @@ async def to_code(config): conf = config[CONF_FRAMEWORK] cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) + if CONF_SOURCE in conf: + cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]]) if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]: cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC") @@ -850,8 +786,6 @@ async def to_code(config): cg.add_build_flag("-Wno-nonnull-compare") - cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]]) - add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True) add_idf_sdkconfig_option( f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True diff --git a/script/generate-esp32-boards.py b/script/generate-esp32-boards.py index 152a480d23..81b78b04be 100755 --- a/script/generate-esp32-boards.py +++ b/script/generate-esp32-boards.py @@ -7,9 +7,10 @@ import subprocess import sys import tempfile -from esphome.components.esp32 import ESP_IDF_PLATFORM_VERSION as ver +from esphome.components.esp32 import PLATFORM_VERSION_LOOKUP from esphome.helpers import write_file_if_changed +ver = PLATFORM_VERSION_LOOKUP["recommended"] version_str = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" root = Path(__file__).parent.parent boards_file_path = root / "esphome" / "components" / "esp32" / "boards.py" From f26e71bae6d1f1df2b064ea93e36883a0385efc6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 17:16:09 -0500 Subject: [PATCH 50/99] [ci] Fix clang-tidy after Arduino-as-IDF-component migration (#11031) --- .clang-tidy.hash | 2 +- esphome/components/ethernet/ethernet_component.h | 2 +- platformio.ini | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index f61b79de4d..a6222ebd64 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -4368db58e8f884aff245996b1e8b644cc0796c0bb2fa706d5740d40b823d3ac9 +bc4001761441a1f0d32971287398739b2b1c45435440425f5ed5aacf1c1f8c2b diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index c7cb0abb4c..6b4e342df5 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -163,7 +163,7 @@ class EthernetComponent : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern EthernetComponent *global_eth_component; -#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); #endif diff --git a/platformio.ini b/platformio.ini index d97607fac5..a1cc1b3a49 100644 --- a/platformio.ini +++ b/platformio.ini @@ -129,7 +129,7 @@ platform = https://github.com/pioarduino/platform-espressif32/releases/download/ platform_packages = pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.2.1/esp32-3.2.1.zip -framework = arduino +framework = arduino, espidf ; Arduino as an ESP-IDF component 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 @@ -274,6 +274,7 @@ build_unflags = [env:esp32-arduino-tidy] extends = common:esp32-arduino board = esp32dev +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32-arduino-tidy build_flags = ${common:esp32-arduino.build_flags} ${flags:clangtidy.build_flags} From f62e06104effe69316a45c4510730a2144841512 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 19:03:26 -0500 Subject: [PATCH 51/99] [wifi] Optimize logging to reduce flash usage by 284 bytes on ESP8266 (#11022) --- esphome/components/wifi/wifi_component.cpp | 94 ++++++++++--------- .../wifi/wifi_component_esp8266.cpp | 2 +- .../wifi/wifi_component_esp_idf.cpp | 4 +- 3 files changed, 53 insertions(+), 47 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index c86e233059..2e083d4c68 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -284,30 +284,34 @@ void WiFiComponent::setup_ap_config_() { std::string name = App.get_name(); if (name.length() > 32) { if (App.is_name_add_mac_suffix_enabled()) { - name.erase(name.begin() + 25, name.end() - 7); // Remove characters between 25 and the mac address + // Keep first 25 chars and last 7 chars (MAC suffix), remove middle + name.erase(25, name.length() - 32); } else { - name = name.substr(0, 32); + name.resize(32); } } this->ap_.set_ssid(name); } + this->ap_setup_ = this->wifi_start_ap_(this->ap_); + + auto ip_address = this->wifi_soft_ap_ip().str(); ESP_LOGCONFIG(TAG, "Setting up AP:\n" " AP SSID: '%s'\n" - " AP Password: '%s'", - this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str()); - if (this->ap_.get_manual_ip().has_value()) { - auto manual = *this->ap_.get_manual_ip(); + " AP Password: '%s'\n" + " IP Address: %s", + this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), ip_address.c_str()); + + auto manual_ip = this->ap_.get_manual_ip(); + if (manual_ip.has_value()) { ESP_LOGCONFIG(TAG, " AP Static IP: '%s'\n" " AP Gateway: '%s'\n" " AP Subnet: '%s'", - manual.static_ip.str().c_str(), manual.gateway.str().c_str(), manual.subnet.str().c_str()); + manual_ip->static_ip.str().c_str(), manual_ip->gateway.str().c_str(), + manual_ip->subnet.str().c_str()); } - this->ap_setup_ = this->wifi_start_ap_(this->ap_); - ESP_LOGCONFIG(TAG, " IP Address: %s", this->wifi_soft_ap_ip().str().c_str()); - if (!this->has_sta()) { this->state_ = WIFI_COMPONENT_STATE_AP; } @@ -330,9 +334,9 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { } void WiFiComponent::clear_sta() { this->sta_.clear(); } void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) { - SavedWifiSettings save{}; - snprintf(save.ssid, sizeof(save.ssid), "%s", ssid.c_str()); - snprintf(save.password, sizeof(save.password), "%s", password.c_str()); + SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination + strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0 + strncpy(save.password, password.c_str(), sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0 this->pref_.save(&save); // ensure it's written immediately global_preferences->sync(); @@ -349,8 +353,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { ESP_LOGV(TAG, "Connection Params:"); ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str()); if (ap.get_bssid().has_value()) { - bssid_t b = *ap.get_bssid(); - ESP_LOGV(TAG, " BSSID: %02X:%02X:%02X:%02X:%02X:%02X", b[0], b[1], b[2], b[3], b[4], b[5]); + ESP_LOGV(TAG, " BSSID: %s", format_mac_address_pretty(ap.get_bssid()->data()).c_str()); } else { ESP_LOGV(TAG, " BSSID: Not Set"); } @@ -457,7 +460,6 @@ void WiFiComponent::print_connect_params_() { ESP_LOGCONFIG(TAG, " Disabled"); return; } - ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'"), wifi_ssid().c_str()); for (auto &ip : wifi_sta_ip_addresses()) { if (ip.is_set()) { ESP_LOGCONFIG(TAG, " IP Address: %s", ip.str().c_str()); @@ -465,24 +467,23 @@ void WiFiComponent::print_connect_params_() { } int8_t rssi = wifi_rssi(); ESP_LOGCONFIG(TAG, - " BSSID: " LOG_SECRET("%02X:%02X:%02X:%02X:%02X:%02X") "\n" - " Hostname: '%s'\n" - " Signal strength: %d dB %s", - bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], App.get_name().c_str(), rssi, - LOG_STR_ARG(get_signal_bars(rssi))); + " SSID: " LOG_SECRET("'%s'") "\n" + " BSSID: " LOG_SECRET("%s") "\n" + " Hostname: '%s'\n" + " Signal strength: %d dB %s\n" + " Channel: %" PRId32 "\n" + " Subnet: %s\n" + " Gateway: %s\n" + " DNS1: %s\n" + " DNS2: %s", + wifi_ssid().c_str(), format_mac_address_pretty(bssid.data()).c_str(), App.get_name().c_str(), rssi, + LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(), + wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef ESPHOME_LOG_HAS_VERBOSE if (this->selected_ap_.get_bssid().has_value()) { ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*this->selected_ap_.get_bssid())); } #endif - ESP_LOGCONFIG(TAG, - " Channel: %" PRId32 "\n" - " Subnet: %s\n" - " Gateway: %s\n" - " DNS1: %s\n" - " DNS2: %s", - get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), - wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef USE_WIFI_11KV_SUPPORT ESP_LOGCONFIG(TAG, " BTM: %s\n" @@ -568,6 +569,25 @@ static void insertion_sort_scan_results(std::vector &results) { } } +// Helper function to log scan results - marked noinline to prevent re-inlining into loop +__attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) { + char bssid_s[18]; + auto bssid = res.get_bssid(); + format_mac_addr_upper(bssid.data(), bssid_s); + + if (res.get_matches()) { + ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), res.get_is_hidden() ? "(HIDDEN) " : "", + bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi()))); + ESP_LOGD(TAG, + " Channel: %u\n" + " RSSI: %d dB", + res.get_channel(), res.get_rssi()); + } else { + ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, + LOG_STR_ARG(get_signal_bars(res.get_rssi()))); + } +} + void WiFiComponent::check_scanning_finished() { if (!this->scan_done_) { if (millis() - this->action_started_ > 30000) { @@ -602,21 +622,7 @@ void WiFiComponent::check_scanning_finished() { insertion_sort_scan_results(this->scan_result_); for (auto &res : this->scan_result_) { - char bssid_s[18]; - auto bssid = res.get_bssid(); - format_mac_addr_upper(bssid.data(), bssid_s); - - if (res.get_matches()) { - ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), - res.get_is_hidden() ? "(HIDDEN) " : "", bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi()))); - ESP_LOGD(TAG, - " Channel: %u\n" - " RSSI: %d dB", - res.get_channel(), res.get_rssi()); - } else { - ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, - LOG_STR_ARG(get_signal_bars(res.get_rssi()))); - } + log_scan_result(res); } if (!this->scan_result_[0].get_matches()) { diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index ae1daed8b5..3b3b4b139c 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -301,7 +301,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // if we have certs, this must be EAP-TLS ret = wifi_station_set_enterprise_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())); + (uint8_t *) eap.password.c_str(), eap.password.length()); if (ret) { ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed: %d", ret); } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 2d1eba8885..ccec800205 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -408,11 +408,11 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { #if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) 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())); + (uint8_t *) eap.password.c_str(), eap.password.length()); #else 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())); + (uint8_t *) eap.password.c_str(), eap.password.length()); #endif if (err != ESP_OK) { ESP_LOGV(TAG, "set_cert_key failed %d", err); From eea2b6b81bc3f80b58b264f057e3772b7f1b4a25 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 19:04:50 -0500 Subject: [PATCH 52/99] [esp32_ble] Optimize string operations to reduce flash usage by 264 bytes (#11023) --- esphome/components/esp32_ble/ble.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 64cef70de2..0c340c55cc 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -213,15 +213,17 @@ bool ESP32BLE::ble_setup_() { if (this->name_.has_value()) { name = this->name_.value(); if (App.is_name_add_mac_suffix_enabled()) { - name += "-" + get_mac_address().substr(6); + name += "-"; + name += get_mac_address().substr(6); } } else { name = App.get_name(); if (name.length() > 20) { if (App.is_name_add_mac_suffix_enabled()) { - name.erase(name.begin() + 13, name.end() - 7); // Remove characters between 13 and the mac address + // Keep first 13 chars and last 7 chars (MAC suffix), remove middle + name.erase(13, name.length() - 20); } else { - name = name.substr(0, 20); + name.resize(20); } } } From 972987acdff6900d69c9a2557dd9a67800ee6a59 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 19:06:26 -0500 Subject: [PATCH 53/99] [esp32_rmt_led_strip] Fix clang-tidy signed/unsigned comparison warning (#11033) --- esphome/components/esp32_rmt_led_strip/led_strip.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index 344ea35e81..fa43aa5950 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -35,7 +35,7 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size if (symbols_free < RMT_SYMBOLS_PER_BYTE) { return 0; } - for (int32_t i = 0; i < RMT_SYMBOLS_PER_BYTE; i++) { + for (size_t i = 0; i < RMT_SYMBOLS_PER_BYTE; i++) { if (bytes[index] & (1 << (7 - i))) { symbols[i] = params->bit1; } else { From d164c06f01e5e6bb0d03233632d6465689a6a6af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 19:06:43 -0500 Subject: [PATCH 54/99] [sonoff_d1] Fix clang-tidy signed/unsigned comparison warning (#11034) --- esphome/components/sonoff_d1/sonoff_d1.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/sonoff_d1/sonoff_d1.cpp b/esphome/components/sonoff_d1/sonoff_d1.cpp index e3d55681c5..cd09f31dd7 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.cpp +++ b/esphome/components/sonoff_d1/sonoff_d1.cpp @@ -50,7 +50,7 @@ static const char *const TAG = "sonoff_d1"; uint8_t SonoffD1Output::calc_checksum_(const uint8_t *cmd, const size_t len) { uint8_t crc = 0; - for (int i = 2; i < len - 1; i++) { + for (size_t i = 2; i < len - 1; i++) { crc += cmd[i]; } return crc; From 4a99987bfe56d29874cc8979e20837cd99961642 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 19:07:00 -0500 Subject: [PATCH 55/99] [tuya] Fix clang-tidy signed/unsigned comparison warning (#11035) --- esphome/components/tuya/select/tuya_select.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/tuya/select/tuya_select.cpp b/esphome/components/tuya/select/tuya_select.cpp index 07b0ff2815..91ddbc77ec 100644 --- a/esphome/components/tuya/select/tuya_select.cpp +++ b/esphome/components/tuya/select/tuya_select.cpp @@ -50,7 +50,7 @@ void TuyaSelect::dump_config() { " Options are:", this->select_id_, this->is_int_ ? "int" : "enum"); auto options = this->traits.get_options(); - for (auto i = 0; i < this->mappings_.size(); i++) { + for (size_t i = 0; i < this->mappings_.size(); i++) { ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i).c_str()); } } From 118663f9e2b07385c58b5505e85418127dc5f874 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 19:07:52 -0500 Subject: [PATCH 56/99] [web_server] Use IDF web server for ESP32 Arduino builds (#10991) --- .clang-tidy.hash | 2 +- .../captive_portal/captive_portal.cpp | 34 +- .../prometheus/prometheus_handler.cpp | 478 +++++++++--------- .../components/web_server/list_entities.cpp | 7 +- esphome/components/web_server/list_entities.h | 18 +- esphome/components/web_server/ota/__init__.py | 2 +- .../web_server/ota/ota_web_server.cpp | 12 +- esphome/components/web_server/web_server.cpp | 20 +- esphome/components/web_server/web_server.h | 11 +- .../components/web_server/web_server_v1.cpp | 40 +- .../components/web_server_base/__init__.py | 7 +- .../web_server_base/web_server_base.h | 34 +- esphome/components/web_server_idf/__init__.py | 2 +- .../components/web_server_idf/multipart.cpp | 4 +- esphome/components/web_server_idf/multipart.h | 4 +- esphome/components/web_server_idf/utils.cpp | 4 +- esphome/components/web_server_idf/utils.h | 4 +- .../web_server_idf/web_server_idf.cpp | 4 +- .../web_server_idf/web_server_idf.h | 10 +- platformio.ini | 6 +- 20 files changed, 363 insertions(+), 340 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index a6222ebd64..f2b148342f 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -bc4001761441a1f0d32971287398739b2b1c45435440425f5ed5aacf1c1f8c2b +499db61c1aa55b98b6629df603a56a1ba7aff5a9a7c781a5c1552a9dcd186c08 diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 20abc6506d..30438747f2 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -11,14 +11,14 @@ namespace captive_portal { static const char *const TAG = "captive_portal"; void CaptivePortal::handle_config(AsyncWebServerRequest *request) { - AsyncResponseStream *stream = request->beginResponseStream(F("application/json")); - stream->addHeader(F("cache-control"), F("public, max-age=0, must-revalidate")); + AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("application/json")); + stream->addHeader(ESPHOME_F("cache-control"), ESPHOME_F("public, max-age=0, must-revalidate")); #ifdef USE_ESP8266 - stream->print(F("{\"mac\":\"")); + stream->print(ESPHOME_F("{\"mac\":\"")); stream->print(get_mac_address_pretty().c_str()); - stream->print(F("\",\"name\":\"")); + stream->print(ESPHOME_F("\",\"name\":\"")); stream->print(App.get_name().c_str()); - stream->print(F("\",\"aps\":[{}")); + stream->print(ESPHOME_F("\",\"aps\":[{}")); #else stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str()); #endif @@ -29,19 +29,19 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) { // Assumes no " in ssid, possible unicode isses? #ifdef USE_ESP8266 - stream->print(F(",{\"ssid\":\"")); + stream->print(ESPHOME_F(",{\"ssid\":\"")); stream->print(scan.get_ssid().c_str()); - stream->print(F("\",\"rssi\":")); + stream->print(ESPHOME_F("\",\"rssi\":")); stream->print(scan.get_rssi()); - stream->print(F(",\"lock\":")); + stream->print(ESPHOME_F(",\"lock\":")); stream->print(scan.get_with_auth()); - stream->print(F("}")); + stream->print(ESPHOME_F("}")); #else stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(), scan.get_with_auth()); #endif } - stream->print(F("]}")); + stream->print(ESPHOME_F("]}")); request->send(stream); } void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { @@ -52,7 +52,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); wifi::global_wifi_component->save_wifi_sta(ssid, psk); wifi::global_wifi_component->start_scanning(); - request->redirect(F("/?save")); + request->redirect(ESPHOME_F("/?save")); } void CaptivePortal::setup() { @@ -75,7 +75,7 @@ void CaptivePortal::start() { #ifdef USE_ARDUINO this->dns_server_ = make_unique(); this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); - this->dns_server_->start(53, F("*"), ip); + this->dns_server_->start(53, ESPHOME_F("*"), ip); #endif this->initialized_ = true; @@ -88,10 +88,10 @@ void CaptivePortal::start() { } void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { - if (req->url() == F("/config.json")) { + if (req->url() == ESPHOME_F("/config.json")) { this->handle_config(req); return; - } else if (req->url() == F("/wifisave")) { + } else if (req->url() == ESPHOME_F("/wifisave")) { this->handle_wifisave(req); return; } @@ -100,11 +100,11 @@ void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { // This includes OS captive portal detection endpoints which will trigger // the captive portal when they don't receive their expected responses #ifndef USE_ESP8266 - auto *response = req->beginResponse(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); + auto *response = req->beginResponse(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); #else - auto *response = req->beginResponse_P(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); + auto *response = req->beginResponse_P(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); #endif - response->addHeader(F("Content-Encoding"), F("gzip")); + response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip")); req->send(response); } diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 2677860c7c..68ef18e5ce 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -110,21 +110,21 @@ std::string PrometheusHandler::relabel_name_(EntityBase *obj) { void PrometheusHandler::add_area_label_(AsyncResponseStream *stream, std::string &area) { if (!area.empty()) { - stream->print(F("\",area=\"")); + stream->print(ESPHOME_F("\",area=\"")); stream->print(area.c_str()); } } void PrometheusHandler::add_node_label_(AsyncResponseStream *stream, std::string &node) { if (!node.empty()) { - stream->print(F("\",node=\"")); + stream->print(ESPHOME_F("\",node=\"")); stream->print(node.c_str()); } } void PrometheusHandler::add_friendly_name_label_(AsyncResponseStream *stream, std::string &friendly_name) { if (!friendly_name.empty()) { - stream->print(F("\",friendly_name=\"")); + stream->print(ESPHOME_F("\",friendly_name=\"")); stream->print(friendly_name.c_str()); } } @@ -132,8 +132,8 @@ void PrometheusHandler::add_friendly_name_label_(AsyncResponseStream *stream, st // Type-specific implementation #ifdef USE_SENSOR void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_sensor_value gauge\n")); - stream->print(F("#TYPE esphome_sensor_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_sensor_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_sensor_failed gauge\n")); } void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj, std::string &area, std::string &node, std::string &friendly_name) { @@ -141,37 +141,37 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor return; if (!std::isnan(obj->state)) { // We have a valid value, output this value - stream->print(F("esphome_sensor_failed{id=\"")); + stream->print(ESPHOME_F("esphome_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_sensor_value{id=\"")); + stream->print(ESPHOME_F("esphome_sensor_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",unit=\"")); + stream->print(ESPHOME_F("\",unit=\"")); stream->print(obj->get_unit_of_measurement().c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(value_accuracy_to_string(obj->state, obj->get_accuracy_decimals()).c_str()); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } else { // Invalid state - stream->print(F("esphome_sensor_failed{id=\"")); + stream->print(ESPHOME_F("esphome_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif @@ -179,8 +179,8 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor // Type-specific implementation #ifdef USE_BINARY_SENSOR void PrometheusHandler::binary_sensor_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_binary_sensor_value gauge\n")); - stream->print(F("#TYPE esphome_binary_sensor_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_binary_sensor_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_binary_sensor_failed gauge\n")); } void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj, std::string &area, std::string &node, std::string &friendly_name) { @@ -188,204 +188,204 @@ void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_s return; if (obj->has_state()) { // We have a valid value, output this value - stream->print(F("esphome_binary_sensor_failed{id=\"")); + stream->print(ESPHOME_F("esphome_binary_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_binary_sensor_value{id=\"")); + stream->print(ESPHOME_F("esphome_binary_sensor_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->state); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } else { // Invalid state - stream->print(F("esphome_binary_sensor_failed{id=\"")); + stream->print(ESPHOME_F("esphome_binary_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif #ifdef USE_FAN void PrometheusHandler::fan_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_fan_value gauge\n")); - stream->print(F("#TYPE esphome_fan_failed gauge\n")); - stream->print(F("#TYPE esphome_fan_speed gauge\n")); - stream->print(F("#TYPE esphome_fan_oscillation gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_fan_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_fan_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_fan_speed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_fan_oscillation gauge\n")); } void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj, std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; - stream->print(F("esphome_fan_failed{id=\"")); + stream->print(ESPHOME_F("esphome_fan_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_fan_value{id=\"")); + stream->print(ESPHOME_F("esphome_fan_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->state); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); // Speed if available if (obj->get_traits().supports_speed()) { - stream->print(F("esphome_fan_speed{id=\"")); + stream->print(ESPHOME_F("esphome_fan_speed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->speed); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } // Oscillation if available if (obj->get_traits().supports_oscillation()) { - stream->print(F("esphome_fan_oscillation{id=\"")); + stream->print(ESPHOME_F("esphome_fan_oscillation{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->oscillating); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } } #endif #ifdef USE_LIGHT void PrometheusHandler::light_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_light_state gauge\n")); - stream->print(F("#TYPE esphome_light_color gauge\n")); - stream->print(F("#TYPE esphome_light_effect_active gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_light_state gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_light_color gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_light_effect_active gauge\n")); } void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj, std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; // State - stream->print(F("esphome_light_state{id=\"")); + stream->print(ESPHOME_F("esphome_light_state{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->remote_values.is_on()); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); // Brightness and RGBW light::LightColorValues color = obj->current_values; float brightness, r, g, b, w; color.as_brightness(&brightness); color.as_rgbw(&r, &g, &b, &w); - stream->print(F("esphome_light_color{id=\"")); + stream->print(ESPHOME_F("esphome_light_color{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",channel=\"brightness\"} ")); + stream->print(ESPHOME_F("\",channel=\"brightness\"} ")); stream->print(brightness); - stream->print(F("\n")); - stream->print(F("esphome_light_color{id=\"")); + stream->print(ESPHOME_F("\n")); + stream->print(ESPHOME_F("esphome_light_color{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",channel=\"r\"} ")); + stream->print(ESPHOME_F("\",channel=\"r\"} ")); stream->print(r); - stream->print(F("\n")); - stream->print(F("esphome_light_color{id=\"")); + stream->print(ESPHOME_F("\n")); + stream->print(ESPHOME_F("esphome_light_color{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",channel=\"g\"} ")); + stream->print(ESPHOME_F("\",channel=\"g\"} ")); stream->print(g); - stream->print(F("\n")); - stream->print(F("esphome_light_color{id=\"")); + stream->print(ESPHOME_F("\n")); + stream->print(ESPHOME_F("esphome_light_color{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",channel=\"b\"} ")); + stream->print(ESPHOME_F("\",channel=\"b\"} ")); stream->print(b); - stream->print(F("\n")); - stream->print(F("esphome_light_color{id=\"")); + stream->print(ESPHOME_F("\n")); + stream->print(ESPHOME_F("esphome_light_color{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",channel=\"w\"} ")); + stream->print(ESPHOME_F("\",channel=\"w\"} ")); stream->print(w); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); // Effect std::string effect = obj->get_effect_name(); if (effect == "None") { - stream->print(F("esphome_light_effect_active{id=\"")); + stream->print(ESPHOME_F("esphome_light_effect_active{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",effect=\"None\"} 0\n")); + stream->print(ESPHOME_F("\",effect=\"None\"} 0\n")); } else { - stream->print(F("esphome_light_effect_active{id=\"")); + stream->print(ESPHOME_F("esphome_light_effect_active{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",effect=\"")); + stream->print(ESPHOME_F("\",effect=\"")); stream->print(effect.c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif #ifdef USE_COVER void PrometheusHandler::cover_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_cover_value gauge\n")); - stream->print(F("#TYPE esphome_cover_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_cover_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_cover_failed gauge\n")); } void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj, std::string &area, std::string &node, std::string &friendly_name) { @@ -393,118 +393,118 @@ void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *ob return; if (!std::isnan(obj->position)) { // We have a valid value, output this value - stream->print(F("esphome_cover_failed{id=\"")); + stream->print(ESPHOME_F("esphome_cover_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_cover_value{id=\"")); + stream->print(ESPHOME_F("esphome_cover_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->position); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); if (obj->get_traits().get_supports_tilt()) { - stream->print(F("esphome_cover_tilt{id=\"")); + stream->print(ESPHOME_F("esphome_cover_tilt{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->tilt); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } } else { // Invalid state - stream->print(F("esphome_cover_failed{id=\"")); + stream->print(ESPHOME_F("esphome_cover_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif #ifdef USE_SWITCH void PrometheusHandler::switch_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_switch_value gauge\n")); - stream->print(F("#TYPE esphome_switch_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_switch_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_switch_failed gauge\n")); } void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj, std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; - stream->print(F("esphome_switch_failed{id=\"")); + stream->print(ESPHOME_F("esphome_switch_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_switch_value{id=\"")); + stream->print(ESPHOME_F("esphome_switch_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->state); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } #endif #ifdef USE_LOCK void PrometheusHandler::lock_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_lock_value gauge\n")); - stream->print(F("#TYPE esphome_lock_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_lock_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_lock_failed gauge\n")); } void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj, std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; - stream->print(F("esphome_lock_failed{id=\"")); + stream->print(ESPHOME_F("esphome_lock_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_lock_value{id=\"")); + stream->print(ESPHOME_F("esphome_lock_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->state); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } #endif // Type-specific implementation #ifdef USE_TEXT_SENSOR void PrometheusHandler::text_sensor_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_text_sensor_value gauge\n")); - stream->print(F("#TYPE esphome_text_sensor_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_text_sensor_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_text_sensor_failed gauge\n")); } void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_sensor::TextSensor *obj, std::string &area, std::string &node, std::string &friendly_name) { @@ -512,37 +512,37 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso return; if (obj->has_state()) { // We have a valid value, output this value - stream->print(F("esphome_text_sensor_failed{id=\"")); + stream->print(ESPHOME_F("esphome_text_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_text_sensor_value{id=\"")); + stream->print(ESPHOME_F("esphome_text_sensor_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",value=\"")); + stream->print(ESPHOME_F("\",value=\"")); stream->print(obj->state.c_str()); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); } else { // Invalid state - stream->print(F("esphome_text_sensor_failed{id=\"")); + stream->print(ESPHOME_F("esphome_text_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif @@ -550,8 +550,8 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso // Type-specific implementation #ifdef USE_NUMBER void PrometheusHandler::number_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_number_value gauge\n")); - stream->print(F("#TYPE esphome_number_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_number_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_number_failed gauge\n")); } void PrometheusHandler::number_row_(AsyncResponseStream *stream, number::Number *obj, std::string &area, std::string &node, std::string &friendly_name) { @@ -559,43 +559,43 @@ void PrometheusHandler::number_row_(AsyncResponseStream *stream, number::Number return; if (!std::isnan(obj->state)) { // We have a valid value, output this value - stream->print(F("esphome_number_failed{id=\"")); + stream->print(ESPHOME_F("esphome_number_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_number_value{id=\"")); + stream->print(ESPHOME_F("esphome_number_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->state); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } else { // Invalid state - stream->print(F("esphome_number_failed{id=\"")); + stream->print(ESPHOME_F("esphome_number_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif #ifdef USE_SELECT void PrometheusHandler::select_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_select_value gauge\n")); - stream->print(F("#TYPE esphome_select_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_select_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_select_failed gauge\n")); } void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select *obj, std::string &area, std::string &node, std::string &friendly_name) { @@ -603,105 +603,105 @@ void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select return; if (obj->has_state()) { // We have a valid value, output this value - stream->print(F("esphome_select_failed{id=\"")); + stream->print(ESPHOME_F("esphome_select_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_select_value{id=\"")); + stream->print(ESPHOME_F("esphome_select_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",value=\"")); + stream->print(ESPHOME_F("\",value=\"")); stream->print(obj->state.c_str()); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); } else { // Invalid state - stream->print(F("esphome_select_failed{id=\"")); + stream->print(ESPHOME_F("esphome_select_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif #ifdef USE_MEDIA_PLAYER void PrometheusHandler::media_player_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_media_player_state_value gauge\n")); - stream->print(F("#TYPE esphome_media_player_volume gauge\n")); - stream->print(F("#TYPE esphome_media_player_is_muted gauge\n")); - stream->print(F("#TYPE esphome_media_player_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_media_player_state_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_media_player_volume gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_media_player_is_muted gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_media_player_failed gauge\n")); } void PrometheusHandler::media_player_row_(AsyncResponseStream *stream, media_player::MediaPlayer *obj, std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; - stream->print(F("esphome_media_player_failed{id=\"")); + stream->print(ESPHOME_F("esphome_media_player_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_media_player_state_value{id=\"")); + stream->print(ESPHOME_F("esphome_media_player_state_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",value=\"")); + stream->print(ESPHOME_F("\",value=\"")); stream->print(media_player::media_player_state_to_string(obj->state)); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); - stream->print(F("esphome_media_player_volume{id=\"")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); + stream->print(ESPHOME_F("esphome_media_player_volume{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->volume); - stream->print(F("\n")); - stream->print(F("esphome_media_player_is_muted{id=\"")); + stream->print(ESPHOME_F("\n")); + stream->print(ESPHOME_F("esphome_media_player_is_muted{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); if (obj->is_muted()) { - stream->print(F("1.0")); + stream->print(ESPHOME_F("1.0")); } else { - stream->print(F("0.0")); + stream->print(ESPHOME_F("0.0")); } - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } #endif #ifdef USE_UPDATE void PrometheusHandler::update_entity_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_update_entity_state gauge\n")); - stream->print(F("#TYPE esphome_update_entity_info gauge\n")); - stream->print(F("#TYPE esphome_update_entity_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_update_entity_state gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_update_entity_info gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_update_entity_failed gauge\n")); } void PrometheusHandler::handle_update_state_(AsyncResponseStream *stream, update::UpdateState state) { @@ -730,168 +730,168 @@ void PrometheusHandler::update_entity_row_(AsyncResponseStream *stream, update:: return; if (obj->has_state()) { // We have a valid value, output this value - stream->print(F("esphome_update_entity_failed{id=\"")); + stream->print(ESPHOME_F("esphome_update_entity_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // First update state - stream->print(F("esphome_update_entity_state{id=\"")); + stream->print(ESPHOME_F("esphome_update_entity_state{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",value=\"")); + stream->print(ESPHOME_F("\",value=\"")); handle_update_state_(stream, obj->state); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); // Next update info - stream->print(F("esphome_update_entity_info{id=\"")); + stream->print(ESPHOME_F("esphome_update_entity_info{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",current_version=\"")); + stream->print(ESPHOME_F("\",current_version=\"")); stream->print(obj->update_info.current_version.c_str()); - stream->print(F("\",latest_version=\"")); + stream->print(ESPHOME_F("\",latest_version=\"")); stream->print(obj->update_info.latest_version.c_str()); - stream->print(F("\",title=\"")); + stream->print(ESPHOME_F("\",title=\"")); stream->print(obj->update_info.title.c_str()); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); } else { // Invalid state - stream->print(F("esphome_update_entity_failed{id=\"")); + stream->print(ESPHOME_F("esphome_update_entity_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif #ifdef USE_VALVE void PrometheusHandler::valve_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_valve_operation gauge\n")); - stream->print(F("#TYPE esphome_valve_failed gauge\n")); - stream->print(F("#TYPE esphome_valve_position gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_valve_operation gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_valve_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_valve_position gauge\n")); } void PrometheusHandler::valve_row_(AsyncResponseStream *stream, valve::Valve *obj, std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; - stream->print(F("esphome_valve_failed{id=\"")); + stream->print(ESPHOME_F("esphome_valve_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_valve_operation{id=\"")); + stream->print(ESPHOME_F("esphome_valve_operation{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",operation=\"")); + stream->print(ESPHOME_F("\",operation=\"")); stream->print(valve::valve_operation_to_str(obj->current_operation)); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); // Now see if position is supported if (obj->get_traits().get_supports_position()) { - stream->print(F("esphome_valve_position{id=\"")); + stream->print(ESPHOME_F("esphome_valve_position{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->position); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } } #endif #ifdef USE_CLIMATE void PrometheusHandler::climate_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_climate_setting gauge\n")); - stream->print(F("#TYPE esphome_climate_value gauge\n")); - stream->print(F("#TYPE esphome_climate_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_climate_setting gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_climate_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_climate_failed gauge\n")); } void PrometheusHandler::climate_setting_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node, std::string &friendly_name, std::string &setting, const LogString *setting_value) { - stream->print(F("esphome_climate_setting{id=\"")); + stream->print(ESPHOME_F("esphome_climate_setting{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",category=\"")); + stream->print(ESPHOME_F("\",category=\"")); stream->print(setting.c_str()); - stream->print(F("\",setting_value=\"")); + stream->print(ESPHOME_F("\",setting_value=\"")); stream->print(LOG_STR_ARG(setting_value)); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); } void PrometheusHandler::climate_value_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node, std::string &friendly_name, std::string &category, std::string &climate_value) { - stream->print(F("esphome_climate_value{id=\"")); + stream->print(ESPHOME_F("esphome_climate_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",category=\"")); + stream->print(ESPHOME_F("\",category=\"")); stream->print(category.c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(climate_value.c_str()); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } void PrometheusHandler::climate_failed_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node, std::string &friendly_name, std::string &category, bool is_failed_value) { - stream->print(F("esphome_climate_failed{id=\"")); + stream->print(ESPHOME_F("esphome_climate_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",category=\"")); + stream->print(ESPHOME_F("\",category=\"")); stream->print(category.c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); if (is_failed_value) { - stream->print(F("1.0")); + stream->print(ESPHOME_F("1.0")); } else { - stream->print(F("0.0")); + stream->print(ESPHOME_F("0.0")); } - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index fb02821760..3eb3764857 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -9,13 +9,12 @@ namespace esphome { namespace web_server { -#ifdef USE_ARDUINO +#ifdef USE_ESP32 +ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, AsyncEventSource *es) : web_server_(ws), events_(es) {} +#elif USE_ARDUINO ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es) : web_server_(ws), events_(es) {} #endif -#ifdef USE_ESP_IDF -ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, AsyncEventSource *es) : web_server_(ws), events_(es) {} -#endif ListEntitiesIterator::~ListEntitiesIterator() {} #ifdef USE_BINARY_SENSOR diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index ba81c70c86..43e1cc2544 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -5,25 +5,24 @@ #include "esphome/core/component.h" #include "esphome/core/component_iterator.h" namespace esphome { -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 namespace web_server_idf { class AsyncEventSource; } #endif namespace web_server { -#ifdef USE_ARDUINO +#if !defined(USE_ESP32) && defined(USE_ARDUINO) class DeferredUpdateEventSource; #endif class WebServer; class ListEntitiesIterator : public ComponentIterator { public: -#ifdef USE_ARDUINO - ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es); -#endif -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 ListEntitiesIterator(const WebServer *ws, esphome::web_server_idf::AsyncEventSource *es); +#elif defined(USE_ARDUINO) + ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es); #endif virtual ~ListEntitiesIterator(); #ifdef USE_BINARY_SENSOR @@ -90,11 +89,10 @@ class ListEntitiesIterator : public ComponentIterator { protected: const WebServer *web_server_; -#ifdef USE_ARDUINO - DeferredUpdateEventSource *events_; -#endif -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 esphome::web_server_idf::AsyncEventSource *events_; +#elif USE_ARDUINO + DeferredUpdateEventSource *events_; #endif }; diff --git a/esphome/components/web_server/ota/__init__.py b/esphome/components/web_server/ota/__init__.py index 22e56639e1..4a98db8877 100644 --- a/esphome/components/web_server/ota/__init__.py +++ b/esphome/components/web_server/ota/__init__.py @@ -29,5 +29,5 @@ async def to_code(config): await ota_to_code(var, config) await cg.register_component(var, config) cg.add_define("USE_WEBSERVER_OTA") - if CORE.using_esp_idf: + if CORE.is_esp32: 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 index 672a9868c5..7929f3647f 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -17,6 +17,12 @@ #endif #endif // USE_ARDUINO +#if USE_ESP32 +using PlatformString = std::string; +#elif USE_ARDUINO +using PlatformString = String; +#endif + namespace esphome { namespace web_server { @@ -26,8 +32,8 @@ 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; + void handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data, + size_t len, bool final) override; bool canHandle(AsyncWebServerRequest *request) const override { // Check if this is an OTA update request bool is_ota_request = request->url() == "/update" && request->method() == HTTP_POST; @@ -100,7 +106,7 @@ void OTARequestHandler::ota_init_(const char *filename) { this->ota_success_ = false; } -void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, +void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data, size_t len, bool final) { ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_OK; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 2df74e023e..cfd5fc947b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -8,7 +8,7 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -#ifdef USE_ARDUINO +#if !defined(USE_ESP32) && defined(USE_ARDUINO) #include "StreamString.h" #endif @@ -103,7 +103,7 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain) return match; } -#ifdef USE_ARDUINO +#if !defined(USE_ESP32) && defined(USE_ARDUINO) // helper for allowing only unique entries in the queue void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) { DeferredEvent item(source, message_generator); @@ -301,7 +301,7 @@ void WebServer::setup() { } #endif -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 this->base_->add_handler(&this->events_); #endif this->base_->add_handler(this); @@ -1266,7 +1266,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value #endif // Longest: HORIZONTAL -#define PSTR_LOCAL(mode_s) strncpy_P(buf, (PGM_P) ((mode_s)), 15) +#define PSTR_LOCAL(mode_s) ESPHOME_strncpy_P(buf, (ESPHOME_PGM_P) ((mode_s)), 15) #ifdef USE_CLIMATE void WebServer::on_climate_update(climate::Climate *obj) { @@ -1776,15 +1776,15 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const { // Static URL checks static const char *const STATIC_URLS[] = { - "/", -#ifdef USE_ARDUINO - "/events", + "/", +#if !defined(USE_ESP32) && defined(USE_ARDUINO) + "/events", #endif #ifdef USE_WEBSERVER_CSS_INCLUDE - "/0.css", + "/0.css", #endif #ifdef USE_WEBSERVER_JS_INCLUDE - "/0.js", + "/0.js", #endif }; @@ -1905,7 +1905,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } -#ifdef USE_ARDUINO +#if !defined(USE_ESP32) && defined(USE_ARDUINO) if (url == "/events") { this->events_.add_new_client(this, request); return; diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index e42c35b32d..2e5d58d375 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -81,7 +81,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; implemented in a more straightforward way for ESP-IDF. Arduino platform will eventually go away and this workaround can be forgotten. */ -#ifdef USE_ARDUINO +#if !defined(USE_ESP32) && defined(USE_ARDUINO) using message_generator_t = std::string(WebServer *, void *); class DeferredUpdateEventSourceList; @@ -164,7 +164,7 @@ class DeferredUpdateEventSourceList : public std::listjs_url_ = js_url; } void WebServer::handle_index_request(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream("text/html"); const std::string &title = App.get_name(); - stream->print(F("")); + stream->print(ESPHOME_F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><meta " + "name=viewport content=\"width=device-width, initial-scale=1,user-scalable=no\"><title>")); stream->print(title.c_str()); - stream->print(F("")); + stream->print(ESPHOME_F("")); #ifdef USE_WEBSERVER_CSS_INCLUDE - stream->print(F("")); + stream->print(ESPHOME_F("")); #endif if (strlen(this->css_url_) > 0) { - stream->print(F(R"(print(ESPHOME_F(R"(print(this->css_url_); - stream->print(F("\">")); + stream->print(ESPHOME_F("\">")); } - stream->print(F("")); - stream->print(F("

")); + stream->print(ESPHOME_F("")); + stream->print(ESPHOME_F("

")); stream->print(title.c_str()); - stream->print(F("

")); - stream->print(F("

States

")); + stream->print(ESPHOME_F("")); + stream->print(ESPHOME_F("

States

NameStateActions
")); #ifdef USE_SENSOR for (auto *obj : App.get_sensors()) { @@ -190,26 +190,28 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { } #endif - stream->print(F("
NameStateActions

See ESPHome Web API for " - "REST API documentation.

")); + stream->print( + ESPHOME_F("

See ESPHome Web API for " + "REST API documentation.

")); #if defined(USE_WEBSERVER_OTA) && !defined(USE_WEBSERVER_OTA_DISABLED) // Show OTA form only if web_server OTA is not explicitly disabled // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal - stream->print(F("

OTA Update

")); + stream->print( + ESPHOME_F("

OTA Update

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

Debug Log

"));
+  stream->print(ESPHOME_F("

Debug Log

"));
 #ifdef USE_WEBSERVER_JS_INCLUDE
   if (this->js_include_ != nullptr) {
-    stream->print(F(""));
+    stream->print(ESPHOME_F(""));
   }
 #endif
   if (strlen(this->js_url_) > 0) {
-    stream->print(F(""));
+    stream->print(ESPHOME_F("\">"));
   }
-  stream->print(F("
")); + stream->print(ESPHOME_F("

")); request->send(stream); } diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index a82ec462d9..4cf76eba0e 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -9,10 +9,10 @@ DEPENDENCIES = ["network"] def AUTO_LOAD(): + if CORE.is_esp32: + return ["web_server_idf"] if CORE.using_arduino: return ["async_tcp"] - if CORE.using_esp_idf: - return ["web_server_idf"] return [] @@ -33,6 +33,9 @@ async def to_code(config): await cg.register_component(var, config) cg.add(cg.RawExpression(f"{web_server_base_ns}::global_web_server_base = {var}")) + if CORE.is_esp32: + return + if CORE.using_arduino: if CORE.is_esp32: cg.add_library("WiFi", None) diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index cfca776ee1..039a452d64 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -7,11 +7,31 @@ #include "esphome/core/component.h" -#ifdef USE_ARDUINO -#include -#elif USE_ESP_IDF +// Platform-agnostic macros for web server components +// On ESP32 (both Arduino and IDF): Use plain strings (no PROGMEM) +// On ESP8266: Use Arduino's F() macro for PROGMEM strings +#ifdef USE_ESP32 +#define ESPHOME_F(string_literal) (string_literal) +#define ESPHOME_PGM_P const char * +#define ESPHOME_strncpy_P strncpy +#else +// ESP8266 uses Arduino macros +#define ESPHOME_F(string_literal) F(string_literal) +#define ESPHOME_PGM_P PGM_P +#define ESPHOME_strncpy_P strncpy_P +#endif + +#if USE_ESP32 #include "esphome/core/hal.h" #include "esphome/components/web_server_idf/web_server_idf.h" +#else +#include +#endif + +#if USE_ESP32 +using PlatformString = std::string; +#elif USE_ARDUINO +using PlatformString = String; #endif namespace esphome { @@ -28,8 +48,8 @@ class MiddlewareHandler : public AsyncWebHandler { 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 { + void handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data, + size_t len, bool final) override { next_->handleUpload(request, filename, index, data, len, final); } void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override { @@ -65,8 +85,8 @@ class AuthMiddlewareHandler : public MiddlewareHandler { return; MiddlewareHandler::handleRequest(request); } - void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, - bool final) override { + void handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data, + size_t len, bool final) override { if (!check_auth(request)) return; MiddlewareHandler::handleUpload(request, filename, index, data, len, final); diff --git a/esphome/components/web_server_idf/__init__.py b/esphome/components/web_server_idf/__init__.py index 506e1c5c13..74a9d657a6 100644 --- a/esphome/components/web_server_idf/__init__.py +++ b/esphome/components/web_server_idf/__init__.py @@ -5,7 +5,7 @@ CODEOWNERS = ["@dentra"] CONFIG_SCHEMA = cv.All( cv.Schema({}), - cv.only_with_esp_idf, + cv.only_on_esp32, ) diff --git a/esphome/components/web_server_idf/multipart.cpp b/esphome/components/web_server_idf/multipart.cpp index 8655226ab9..2092a41a8e 100644 --- a/esphome/components/web_server_idf/multipart.cpp +++ b/esphome/components/web_server_idf/multipart.cpp @@ -1,5 +1,5 @@ #include "esphome/core/defines.h" -#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) +#if defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) #include "multipart.h" #include "utils.h" #include "esphome/core/log.h" @@ -251,4 +251,4 @@ std::string str_trim(const std::string &str) { } // namespace web_server_idf } // namespace esphome -#endif // defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) +#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) diff --git a/esphome/components/web_server_idf/multipart.h b/esphome/components/web_server_idf/multipart.h index 967c72ffa5..8fbe90c4a0 100644 --- a/esphome/components/web_server_idf/multipart.h +++ b/esphome/components/web_server_idf/multipart.h @@ -1,6 +1,6 @@ #pragma once #include "esphome/core/defines.h" -#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) +#if defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) #include #include @@ -83,4 +83,4 @@ std::string str_trim(const std::string &str); } // namespace web_server_idf } // namespace esphome -#endif // defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) +#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index ac5df90bb8..d5d34b520b 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -1,4 +1,4 @@ -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #include #include @@ -122,4 +122,4 @@ const char *stristr(const char *haystack, const char *needle) { } // namespace web_server_idf } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index 988b962d72..f70a5f0760 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -1,5 +1,5 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #include @@ -24,4 +24,4 @@ const char *stristr(const char *haystack, const char *needle); } // namespace web_server_idf } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 60a1b8acbf..b38c5fb92a 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -1,4 +1,4 @@ -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #include @@ -761,4 +761,4 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c } // namespace web_server_idf } // namespace esphome -#endif // !defined(USE_ESP_IDF) +#endif // !defined(USE_ESP32) diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 3d482da0a5..bf93dcbd34 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -1,5 +1,5 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/defines.h" #include @@ -22,12 +22,6 @@ class ListEntitiesIterator; #endif namespace web_server_idf { -#define F(string_literal) (string_literal) -#define PGM_P const char * -#define strncpy_P strncpy - -using String = std::string; - class AsyncWebParameter { public: AsyncWebParameter(std::string name, std::string value) : name_(std::move(name)), value_(std::move(value)) {} @@ -349,4 +343,4 @@ class DefaultHeaders { using namespace esphome::web_server_idf; // NOLINT(google-global-names-in-headers) -#endif // !defined(USE_ESP_IDF) +#endif // !defined(USE_ESP32) diff --git a/platformio.ini b/platformio.ini index a1cc1b3a49..70b562adff 100644 --- a/platformio.ini +++ b/platformio.ini @@ -72,7 +72,6 @@ lib_deps = SPI ; spi (Arduino built-in) Wire ; i2c (Arduino built-int) heman/AsyncMqttClient-esphome@1.0.0 ; mqtt - ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base fastled/FastLED@3.9.16 ; fastled_base freekode/TM1651@1.0.1 ; tm1651 glmnet/Dsmr@0.7 ; dsmr @@ -107,6 +106,7 @@ lib_deps = ESP8266WiFi ; wifi (Arduino built-in) Update ; ota (Arduino built-in) ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp + ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base makuna/NeoPixelBus@2.7.3 ; neopixelbus ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in) @@ -193,6 +193,7 @@ platform_packages = framework = arduino lib_deps = ${common:arduino.lib_deps} + ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base build_flags = ${common:arduino.build_flags} -DUSE_RP2040 @@ -207,7 +208,8 @@ platform = libretiny@1.9.1 framework = arduino lib_compat_mode = soft lib_deps = - droscy/esp_wireguard@0.4.2 ; wireguard + ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base + droscy/esp_wireguard@0.4.2 ; wireguard build_flags = ${common:arduino.build_flags} -DUSE_LIBRETINY From da2089c8be4416c8974acb3fb53ac24e3cd73001 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 5 Oct 2025 20:10:05 -0400 Subject: [PATCH 57/99] [core] Remove platformio install from setup (#10997) --- script/setup | 2 -- script/setup.bat | 2 -- 2 files changed, 4 deletions(-) diff --git a/script/setup b/script/setup index 1bd7c44575..8cad7017ff 100755 --- a/script/setup +++ b/script/setup @@ -22,8 +22,6 @@ uv pip install -e ".[dev,test]" --config-settings editable_mode=compat pre-commit install -script/platformio_install_deps.py platformio.ini --libraries --tools --platforms - mkdir -p .temp echo diff --git a/script/setup.bat b/script/setup.bat index f89d5aea1a..003ea31b36 100644 --- a/script/setup.bat +++ b/script/setup.bat @@ -19,8 +19,6 @@ pip3 install -e ".[dev,test]" --config-settings editable_mode=compat pre-commit install -python script/platformio_install_deps.py platformio.ini --libraries --tools --platforms - echo . echo . echo Virtual environment created. Run 'venv/Scripts/activate' to use it. From a3622d878de4ea845ec19ba8575c01eb6ed8af60 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 6 Oct 2025 03:11:36 +0200 Subject: [PATCH 58/99] [nextion] Reduce DEBUG logs on events (#11014) --- esphome/components/nextion/nextion.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index b348bc9920..0ce9d02e97 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -77,7 +77,7 @@ bool Nextion::check_connect_() { this->recv_ret_string_(response, 0, false); if (!response.empty() && response[0] == 0x1A) { // Swallow invalid variable name responses that may be caused by the above commands - ESP_LOGD(TAG, "0x1A error ignored (setup)"); + ESP_LOGV(TAG, "0x1A error ignored (setup)"); return false; } if (response.empty() || response.find("comok") == std::string::npos) { @@ -334,7 +334,7 @@ void Nextion::loop() { 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"); + ESP_LOGV(TAG, "Manual ready set"); this->connection_state_.nextion_reports_is_setup_ = true; } } @@ -544,7 +544,7 @@ void Nextion::process_nextion_commands_() { uint8_t page_id = to_process[0]; uint8_t component_id = to_process[1]; uint8_t touch_event = to_process[2]; // 0 -> release, 1 -> press - ESP_LOGD(TAG, "Touch %s: page %u comp %u", touch_event ? "PRESS" : "RELEASE", page_id, component_id); + ESP_LOGV(TAG, "Touch %s: page %u comp %u", touch_event ? "PRESS" : "RELEASE", page_id, component_id); for (auto *touch : this->touch_) { touch->process_touch(page_id, component_id, touch_event != 0); } @@ -559,7 +559,7 @@ void Nextion::process_nextion_commands_() { } uint8_t page_id = to_process[0]; - ESP_LOGD(TAG, "New page: %u", page_id); + ESP_LOGV(TAG, "New page: %u", page_id); this->page_callback_.call(page_id); break; } @@ -577,7 +577,7 @@ void Nextion::process_nextion_commands_() { 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); + ESP_LOGV(TAG, "Touch %s at %u,%u", touch_event ? "PRESS" : "RELEASE", x, y); break; } @@ -676,7 +676,7 @@ void Nextion::process_nextion_commands_() { } case 0x88: // system successful start up { - ESP_LOGD(TAG, "System start: %zu", to_process_length); + ESP_LOGV(TAG, "System start: %zu", to_process_length); this->connection_state_.nextion_reports_is_setup_ = true; break; } @@ -922,7 +922,7 @@ void Nextion::set_nextion_sensor_state(NextionQueueType queue_type, const std::s } void Nextion::set_nextion_text_state(const std::string &name, const std::string &state) { - ESP_LOGD(TAG, "State: %s='%s'", name.c_str(), state.c_str()); + ESP_LOGV(TAG, "State: %s='%s'", name.c_str(), state.c_str()); for (auto *sensor : this->textsensortype_) { if (name == sensor->get_variable_name()) { @@ -933,7 +933,7 @@ void Nextion::set_nextion_text_state(const std::string &name, const std::string } void Nextion::all_components_send_state_(bool force_update) { - ESP_LOGD(TAG, "Send states"); + ESP_LOGV(TAG, "Send states"); for (auto *binarysensortype : this->binarysensortype_) { if (force_update || binarysensortype->get_needs_to_send_update()) binarysensortype->send_state_to_nextion(); From e8854e0659fc3df951e170f194e7a4e6da552469 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Oct 2025 21:45:44 -0500 Subject: [PATCH 59/99] [esp32_ble] Fix max_connections architecture (shared client+server limit) (#11006) --- esphome/components/ble_client/__init__.py | 2 +- .../components/bluetooth_proxy/__init__.py | 8 +- esphome/components/esp32_ble/__init__.py | 111 +++++++++++++++++- .../esp32_ble_server/ble_characteristic.cpp | 6 +- .../esp32_ble_server/ble_server.cpp | 31 ++++- .../components/esp32_ble_server/ble_server.h | 14 ++- .../components/esp32_ble_tracker/__init__.py | 79 +++---------- esphome/core/defines.h | 1 + 8 files changed, 173 insertions(+), 79 deletions(-) diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 5f4ea8afd1..768a345213 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -116,7 +116,7 @@ CONFIG_SCHEMA = cv.All( ) .extend(cv.COMPONENT_SCHEMA) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA), - esp32_ble_tracker.consume_connection_slots(1, "ble_client"), + esp32_ble.consume_connection_slots(1, "ble_client"), ) CONF_BLE_CLIENT_ID = "ble_client_id" diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index 42a88f1421..ad7528c156 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -42,9 +42,7 @@ def validate_connections(config): ) elif config[CONF_ACTIVE]: connection_slots: int = config[CONF_CONNECTION_SLOTS] - esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")( - config - ) + esp32_ble.consume_connection_slots(connection_slots, "bluetooth_proxy")(config) return { **config, @@ -65,11 +63,11 @@ CONFIG_SCHEMA = cv.All( default=DEFAULT_CONNECTION_SLOTS, ): cv.All( cv.positive_int, - cv.Range(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS), + cv.Range(min=1, max=esp32_ble.IDF_MAX_CONNECTIONS), ), cv.Optional(CONF_CONNECTIONS): cv.All( cv.ensure_list(CONNECTION_SCHEMA), - cv.Length(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS), + cv.Length(min=1, max=esp32_ble.IDF_MAX_CONNECTIONS), ), } ) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 0501d1c5ef..15afb22ab8 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -1,5 +1,8 @@ +from collections.abc import Callable, MutableMapping from enum import Enum +import logging import re +from typing import Any from esphome import automation import esphome.codegen as cg @@ -9,16 +12,19 @@ from esphome.const import ( CONF_ENABLE_ON_BOOT, CONF_ESPHOME, CONF_ID, + CONF_MAX_CONNECTIONS, CONF_NAME, CONF_NAME_ADD_MAC_SUFFIX, ) -from esphome.core import TimePeriod +from esphome.core import CORE, TimePeriod import esphome.final_validate as fv DEPENDENCIES = ["esp32"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] DOMAIN = "esp32_ble" +_LOGGER = logging.getLogger(__name__) + class BTLoggers(Enum): """Bluetooth logger categories available in ESP-IDF. @@ -127,6 +133,28 @@ CONF_DISABLE_BT_LOGS = "disable_bt_logs" CONF_CONNECTION_TIMEOUT = "connection_timeout" CONF_MAX_NOTIFICATIONS = "max_notifications" +# BLE connection limits +# ESP-IDF CONFIG_BT_ACL_CONNECTIONS has range 1-9, default 4 +# Total instances: 10 (ADV + SCAN + connections) +# - ADV only: up to 9 connections +# - SCAN only: up to 9 connections +# - ADV + SCAN: up to 8 connections +DEFAULT_MAX_CONNECTIONS = 3 +IDF_MAX_CONNECTIONS = 9 + +# Connection slot tracking keys +KEY_ESP32_BLE = "esp32_ble" +KEY_USED_CONNECTION_SLOTS = "used_connection_slots" + +# Export for use by other components (bluetooth_proxy, etc.) +__all__ = [ + "DEFAULT_MAX_CONNECTIONS", + "IDF_MAX_CONNECTIONS", + "KEY_ESP32_BLE", + "KEY_USED_CONNECTION_SLOTS", + "consume_connection_slots", +] + NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble") @@ -183,6 +211,9 @@ CONFIG_SCHEMA = cv.Schema( cv.positive_int, cv.Range(min=1, max=64), ), + cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All( + cv.positive_int, cv.Range(min=1, max=IDF_MAX_CONNECTIONS) + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -230,6 +261,56 @@ def validate_variant(_): raise cv.Invalid(f"{variant} does not support Bluetooth") +def consume_connection_slots( + value: int, consumer: str +) -> Callable[[MutableMapping], MutableMapping]: + """Reserve BLE connection slots for a component. + + Args: + value: Number of connection slots to reserve + consumer: Name of the component consuming the slots + + Returns: + A validator function that records the slot usage + """ + + def _consume_connection_slots(config: MutableMapping) -> MutableMapping: + data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE, {}) + slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, []) + slots.extend([consumer] * value) + return config + + return _consume_connection_slots + + +def validate_connection_slots(max_connections: int) -> None: + """Validate that BLE connection slots don't exceed the configured maximum.""" + ble_data = CORE.data.get(KEY_ESP32_BLE, {}) + used_slots = ble_data.get(KEY_USED_CONNECTION_SLOTS, []) + num_used = len(used_slots) + + if num_used <= max_connections: + return + + slot_users = ", ".join(used_slots) + + if num_used > IDF_MAX_CONNECTIONS: + raise cv.Invalid( + f"BLE components require {num_used} connection slots but maximum is {IDF_MAX_CONNECTIONS}. " + f"Reduce the number of BLE clients. Components: {slot_users}" + ) + + _LOGGER.warning( + "BLE components require %d connection slot(s) but only %d configured. " + "Please set 'max_connections: %d' in the 'esp32_ble' component. " + "Components: %s", + num_used, + max_connections, + num_used, + slot_users, + ) + + def final_validation(config): validate_variant(config) if (name := config.get(CONF_NAME)) is not None: @@ -245,6 +326,10 @@ def final_validation(config): # Set GATT Client/Server sdkconfig options based on which components are loaded full_config = fv.full_config.get() + # Validate connection slots usage + max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) + validate_connection_slots(max_connections) + # Check if BLE Server is needed has_ble_server = "esp32_ble_server" in full_config add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server) @@ -255,6 +340,26 @@ def final_validation(config): ) add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client) + # Handle max_connections: check for deprecated location in esp32_ble_tracker + max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) + + # Use value from tracker if esp32_ble doesn't have it explicitly set (backward compat) + if "esp32_ble_tracker" in full_config: + tracker_config = full_config["esp32_ble_tracker"] + if "max_connections" in tracker_config and CONF_MAX_CONNECTIONS not in config: + max_connections = tracker_config["max_connections"] + + # Set CONFIG_BT_ACL_CONNECTIONS to the maximum connections needed + 1 for ADV/SCAN + # This is the Bluedroid host stack total instance limit (range 1-9, default 4) + # Total instances = ADV/SCAN (1) + connection slots (max_connections) + # Shared between client (tracker/ble_client) and server + add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", max_connections + 1) + + # Set controller-specific max connections for ESP32 (classic) + # CONFIG_BTDM_CTRL_BLE_MAX_CONN is ESP32-specific controller limit (just connections, not ADV/SCAN) + # For newer chips (C3/S3/etc), different configs are used automatically + add_idf_sdkconfig_option("CONFIG_BTDM_CTRL_BLE_MAX_CONN", max_connections) + return config @@ -270,6 +375,10 @@ async def to_code(config): cg.add(var.set_name(name)) await cg.register_component(var, config) + # Define max connections for use in C++ code (e.g., ble_server.h) + max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) + cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections) + add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True) diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index d485d9fe2d..c632165fb7 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -49,7 +49,11 @@ void BLECharacteristic::notify() { this->service_->get_server()->get_connected_client_count() == 0) return; - for (auto &client : this->service_->get_server()->get_clients()) { + const uint16_t *clients = this->service_->get_server()->get_clients(); + uint8_t client_count = this->service_->get_server()->get_client_count(); + + for (uint8_t i = 0; i < client_count; i++) { + uint16_t client = clients[i]; size_t length = this->value_.size(); // Find the client in the list of clients to notify auto *entry = this->find_client_in_notify_list_(client); diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 942be7e597..25cc97eeaf 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -185,9 +185,38 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga } } +int8_t BLEServer::find_client_index_(uint16_t conn_id) const { + for (uint8_t i = 0; i < this->client_count_; i++) { + if (this->clients_[i] == conn_id) + return i; + } + return -1; +} + +void BLEServer::add_client_(uint16_t conn_id) { + // Check if already in list + if (this->find_client_index_(conn_id) >= 0) + return; + // Add if there's space + if (this->client_count_ < USE_ESP32_BLE_MAX_CONNECTIONS) { + this->clients_[this->client_count_++] = conn_id; + } else { + // This should never happen since max clients is known at compile time + ESP_LOGE(TAG, "Client array full"); + } +} + +void BLEServer::remove_client_(uint16_t conn_id) { + int8_t index = this->find_client_index_(conn_id); + if (index >= 0) { + // Replace with last element and decrement count (client order not preserved) + this->clients_[index] = this->clients_[--this->client_count_]; + } +} + void BLEServer::ble_before_disabled_event_handler() { // Delete all clients - this->clients_.clear(); + this->client_count_ = 0; // Delete all services for (auto &entry : this->services_) { entry.service->do_delete(); diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index 48005b1346..6fa86dd67f 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -12,7 +12,6 @@ #include #include #include -#include #include #ifdef USE_ESP32 @@ -47,8 +46,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv void set_device_information_service(BLEService *service) { this->device_information_service_ = service; } esp_gatt_if_t get_gatts_if() { return this->gatts_if_; } - uint32_t get_connected_client_count() { return this->clients_.size(); } - const std::unordered_set &get_clients() { return this->clients_; } + uint32_t get_connected_client_count() { return this->client_count_; } + const uint16_t *get_clients() const { return this->clients_; } + uint8_t get_client_count() const { return this->client_count_; } void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) override; @@ -82,8 +82,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv void restart_advertising_(); - void add_client_(uint16_t conn_id) { this->clients_.insert(conn_id); } - void remove_client_(uint16_t conn_id) { this->clients_.erase(conn_id); } + int8_t find_client_index_(uint16_t conn_id) const; + void add_client_(uint16_t conn_id); + void remove_client_(uint16_t conn_id); void dispatch_callbacks_(CallbackType type, uint16_t conn_id); std::vector callbacks_; @@ -92,7 +93,8 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv esp_gatt_if_t gatts_if_{0}; bool registered_{false}; - std::unordered_set clients_; + uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{}; + uint8_t client_count_{0}; std::vector services_{}; std::vector services_to_start_{}; BLEService *device_information_service_{}; diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 8ebee6b0b1..247496ccd9 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -1,14 +1,13 @@ from __future__ import annotations -from collections.abc import Callable, MutableMapping import logging -from typing import Any from esphome import automation import esphome.codegen as cg from esphome.components import esp32_ble from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32_ble import ( + IDF_MAX_CONNECTIONS, BTLoggers, bt_uuid, bt_uuid16_format, @@ -39,18 +38,12 @@ AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] CODEOWNERS = ["@bdraco"] -KEY_ESP32_BLE_TRACKER = "esp32_ble_tracker" -KEY_USED_CONNECTION_SLOTS = "used_connection_slots" - CONF_ESP32_BLE_ID = "esp32_ble_id" CONF_SCAN_PARAMETERS = "scan_parameters" CONF_WINDOW = "window" CONF_ON_SCAN_END = "on_scan_end" CONF_SOFTWARE_COEXISTENCE = "software_coexistence" -DEFAULT_MAX_CONNECTIONS = 3 -IDF_MAX_CONNECTIONS = 9 - _LOGGER = logging.getLogger(__name__) @@ -128,6 +121,15 @@ def validate_scan_parameters(config): return config +def validate_max_connections_deprecated(config: ConfigType) -> ConfigType: + if CONF_MAX_CONNECTIONS in config: + _LOGGER.warning( + "The 'max_connections' option in 'esp32_ble_tracker' is deprecated. " + "Please move it to the 'esp32_ble' component instead." + ) + return config + + def as_hex(value): return cg.RawExpression(f"0x{value}ULL") @@ -150,24 +152,12 @@ def as_reversed_hex_array(value): ) -def consume_connection_slots( - value: int, consumer: str -) -> Callable[[MutableMapping], MutableMapping]: - def _consume_connection_slots(config: MutableMapping) -> MutableMapping: - data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE_TRACKER, {}) - slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, []) - slots.extend([consumer] * value) - return config - - return _consume_connection_slots - - CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32BLETracker), cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), - cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All( + cv.Optional(CONF_MAX_CONNECTIONS): cv.All( cv.positive_int, cv.Range(min=0, max=IDF_MAX_CONNECTIONS) ), cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All( @@ -224,48 +214,11 @@ CONFIG_SCHEMA = cv.All( cv.OnlyWith(CONF_SOFTWARE_COEXISTENCE, "wifi", default=True): bool, } ).extend(cv.COMPONENT_SCHEMA), + validate_max_connections_deprecated, ) -def validate_remaining_connections(config): - data: dict[str, Any] = CORE.data.get(KEY_ESP32_BLE_TRACKER, {}) - slots: list[str] = data.get(KEY_USED_CONNECTION_SLOTS, []) - used_slots = len(slots) - if used_slots <= config[CONF_MAX_CONNECTIONS]: - return config - slot_users = ", ".join(slots) - - if used_slots < IDF_MAX_CONNECTIONS: - _LOGGER.warning( - "esp32_ble_tracker exceeded `%s`: components attempted to consume %d " - "connection slot(s) out of available configured maximum %d connection " - "slot(s); The system automatically increased `%s` to %d to match the " - "number of used connection slot(s) by components: %s.", - CONF_MAX_CONNECTIONS, - used_slots, - config[CONF_MAX_CONNECTIONS], - CONF_MAX_CONNECTIONS, - used_slots, - slot_users, - ) - config[CONF_MAX_CONNECTIONS] = used_slots - return config - - msg = ( - f"esp32_ble_tracker exceeded `{CONF_MAX_CONNECTIONS}`: " - f"components attempted to consume {used_slots} connection slot(s) " - f"out of available configured maximum {config[CONF_MAX_CONNECTIONS]} " - f"connection slot(s); Decrease the number of BLE clients ({slot_users})" - ) - if config[CONF_MAX_CONNECTIONS] < IDF_MAX_CONNECTIONS: - msg += f" or increase {CONF_MAX_CONNECTIONS}` to {used_slots}" - msg += f" to stay under the {IDF_MAX_CONNECTIONS} connection slot(s) limit." - raise cv.Invalid(msg) - - -FINAL_VALIDATE_SCHEMA = cv.All( - validate_remaining_connections, esp32_ble.validate_variant -) +FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant ESP_BLE_DEVICE_SCHEMA = cv.Schema( { @@ -345,10 +298,8 @@ async def to_code(config): # Match arduino CONFIG_BTU_TASK_STACK_SIZE # https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866 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] - ) + # Note: CONFIG_BT_ACL_CONNECTIONS and CONFIG_BTDM_CTRL_BLE_MAX_CONN are now + # configured in esp32_ble component based on max_connections setting 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/core/defines.h b/esphome/core/defines.h index d560007e71..468e9af5fb 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -159,6 +159,7 @@ #define BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE 16 #define USE_CAPTIVE_PORTAL #define USE_ESP32_BLE +#define USE_ESP32_BLE_MAX_CONNECTIONS 3 #define USE_ESP32_BLE_CLIENT #define USE_ESP32_BLE_DEVICE #define USE_ESP32_BLE_SERVER From f757a19e82a545080299ed01827f8e9ab78ab4a3 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:05:44 +1000 Subject: [PATCH 60/99] [mipi] Fix rotation handling (#11010) --- esphome/components/mipi/__init__.py | 6 +---- esphome/components/mipi_spi/display.py | 32 +++++++++++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index f670a5913d..7e687cabaa 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -343,11 +343,7 @@ class DriverChip: ) offset_height = native_height - height - offset_height # Swap default dimensions if swap_xy is set, or if rotation is 90/270 and we are not using a buffer - rotated = not requires_buffer(config) and config.get(CONF_ROTATION, 0) in ( - 90, - 270, - ) - if transform.get(CONF_SWAP_XY) is True or rotated: + if transform.get(CONF_SWAP_XY) is True: width, height = height, width offset_height, offset_width = offset_width, offset_height return width, height, offset_width, offset_height diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index e891e2daad..52b5b86fba 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -380,25 +380,41 @@ def get_instance(config): bus_type = BusTypes[bus_type] buffer_type = cg.uint8 if color_depth == 8 else cg.uint16 frac = denominator(config) - rotation = DISPLAY_ROTATIONS[ + rotation = ( 0 if model.rotation_as_transform(config) else config.get(CONF_ROTATION, 0) - ] + ) templateargs = [ buffer_type, bufferpixels, config[CONF_BYTE_ORDER] == "big_endian", display_pixel_mode, bus_type, - width, - height, - offset_width, - offset_height, ] # If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi if requires_buffer(config): - templateargs.append(rotation) - templateargs.append(frac) + templateargs.extend( + [ + width, + height, + offset_width, + offset_height, + DISPLAY_ROTATIONS[rotation], + frac, + ] + ) return MipiSpiBuffer, templateargs + # Swap height and width if the display is rotated 90 or 270 degrees in software + if rotation in (90, 270): + width, height = height, width + offset_width, offset_height = offset_height, offset_width + templateargs.extend( + [ + width, + height, + offset_width, + offset_height, + ] + ) return MipiSpi, templateargs From cfd241ff29f83b29e616a5d31ed08989a46abef1 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 5 Oct 2025 22:47:55 -0500 Subject: [PATCH 61/99] [zwave_proxy] Send HomeID upon client connect (#11037) --- esphome/components/api/api_connection.cpp | 5 +++ .../components/zwave_proxy/zwave_proxy.cpp | 32 +++++++++++++------ esphome/components/zwave_proxy/zwave_proxy.h | 2 ++ 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4f32112fa6..615b7f5764 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1389,6 +1389,11 @@ void APIConnection::complete_authentication_() { this->send_time_request(); } #endif +#ifdef USE_ZWAVE_PROXY + if (zwave_proxy::global_zwave_proxy != nullptr) { + zwave_proxy::global_zwave_proxy->api_connection_authenticated(this); + } +#endif } bool APIConnection::send_hello_response(const HelloRequest &msg) { diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index 70932da87c..a26a9b2335 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -101,15 +101,7 @@ void ZWaveProxy::process_uart_() { // Store the 4-byte Home ID, which starts at offset 4, and notify connected clients if it changed // The frame parser has already validated the checksum and ensured all bytes are present if (this->set_home_id(&this->buffer_[4])) { - api::ZWaveProxyRequest msg; - msg.type = api::enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE; - msg.data = this->home_id_.data(); - msg.data_len = this->home_id_.size(); - if (api::global_api_server != nullptr) { - // We could add code to manage a second subscription type, but, since this message is - // very infrequent and small, we simply send it to all clients - api::global_api_server->on_zwave_proxy_request(msg); - } + this->send_homeid_changed_msg_(); } } ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr)); @@ -135,6 +127,13 @@ void ZWaveProxy::dump_config() { format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str()); } +void ZWaveProxy::api_connection_authenticated(api::APIConnection *conn) { + if (this->home_id_ready_) { + // If a client just authenticated & HomeID is ready, send the current HomeID + this->send_homeid_changed_msg_(conn); + } +} + void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type) { switch (type) { case api::enums::ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE: @@ -178,6 +177,21 @@ void ZWaveProxy::send_frame(const uint8_t *data, size_t length) { this->write_array(data, length); } +void ZWaveProxy::send_homeid_changed_msg_(api::APIConnection *conn) { + api::ZWaveProxyRequest msg; + msg.type = api::enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE; + msg.data = this->home_id_.data(); + msg.data_len = this->home_id_.size(); + if (conn != nullptr) { + // Send to specific connection + conn->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE); + } else if (api::global_api_server != nullptr) { + // We could add code to manage a second subscription type, but, since this message is + // very infrequent and small, we simply send it to all clients + api::global_api_server->on_zwave_proxy_request(msg); + } +} + void ZWaveProxy::send_simple_command_(const uint8_t command_id) { // Send a simple Z-Wave command with no parameters // Frame format: [SOF][LENGTH][TYPE][CMD][CHECKSUM] diff --git a/esphome/components/zwave_proxy/zwave_proxy.h b/esphome/components/zwave_proxy/zwave_proxy.h index a9123a81ca..20d9090d98 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.h +++ b/esphome/components/zwave_proxy/zwave_proxy.h @@ -49,6 +49,7 @@ class ZWaveProxy : public uart::UARTDevice, public Component { float get_setup_priority() const override; bool can_proceed() override; + void api_connection_authenticated(api::APIConnection *conn); void zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type); api::APIConnection *get_api_connection() { return this->api_connection_; } @@ -61,6 +62,7 @@ class ZWaveProxy : public uart::UARTDevice, public Component { void send_frame(const uint8_t *data, size_t length); protected: + void send_homeid_changed_msg_(api::APIConnection *conn = nullptr); void send_simple_command_(uint8_t command_id); bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer) void parse_start_(uint8_t byte); From c68017ddb4d018f60672999a7f02716f8a2adec8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 09:07:04 -0500 Subject: [PATCH 62/99] [online_image] Fix clang-tidy sign comparison errors (#11041) --- esphome/components/online_image/bmp_image.cpp | 3 ++- esphome/components/online_image/jpeg_image.cpp | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/online_image/bmp_image.cpp b/esphome/components/online_image/bmp_image.cpp index f55c9f1813..676a2efca9 100644 --- a/esphome/components/online_image/bmp_image.cpp +++ b/esphome/components/online_image/bmp_image.cpp @@ -117,7 +117,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { this->paint_index_++; this->current_index_ += 3; index += 3; - if (x == this->width_ - 1 && this->padding_bytes_ > 0) { + size_t last_col = static_cast(this->width_) - 1; + if (x == last_col && this->padding_bytes_ > 0) { index += this->padding_bytes_; this->current_index_ += this->padding_bytes_; } diff --git a/esphome/components/online_image/jpeg_image.cpp b/esphome/components/online_image/jpeg_image.cpp index e5ee3dd8bf..10586091d5 100644 --- a/esphome/components/online_image/jpeg_image.cpp +++ b/esphome/components/online_image/jpeg_image.cpp @@ -25,8 +25,10 @@ static int draw_callback(JPEGDRAW *jpeg) { // to avoid crashing. App.feed_wdt(); size_t position = 0; - for (size_t y = 0; y < jpeg->iHeight; y++) { - for (size_t x = 0; x < jpeg->iWidth; x++) { + size_t height = static_cast(jpeg->iHeight); + size_t width = static_cast(jpeg->iWidth); + for (size_t y = 0; y < height; y++) { + for (size_t x = 0; x < width; x++) { auto rg = decode_value(jpeg->pPixels[position++]); auto ba = decode_value(jpeg->pPixels[position++]); Color color(rg[1], rg[0], ba[1], ba[0]); From 5cf00466012ac54ad1abc30b398c1b1c9866f34a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 09:07:44 -0500 Subject: [PATCH 63/99] [animation] Fix clang-tidy sign comparison errors (#11042) --- esphome/components/animation/animation.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/animation/animation.cpp b/esphome/components/animation/animation.cpp index 6db6f1a7bd..c2ae3b2f76 100644 --- a/esphome/components/animation/animation.cpp +++ b/esphome/components/animation/animation.cpp @@ -26,12 +26,12 @@ uint32_t Animation::get_animation_frame_count() const { return this->animation_f int Animation::get_current_frame() const { return this->current_frame_; } void Animation::next_frame() { this->current_frame_++; - if (loop_count_ && this->current_frame_ == loop_end_frame_ && + if (loop_count_ && static_cast(this->current_frame_) == loop_end_frame_ && (this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) { this->current_frame_ = loop_start_frame_; this->loop_current_iteration_++; } - if (this->current_frame_ >= animation_frame_count_) { + if (static_cast(this->current_frame_) >= animation_frame_count_) { this->loop_current_iteration_ = 1; this->current_frame_ = 0; } From 26ebac8cb88d572ddb1396c2b588dba3d92dc6e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 09:08:33 -0500 Subject: [PATCH 64/99] [bl0906, bl0942] Fix clang-tidy sign comparison errors (#11043) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/bl0906/bl0906.cpp | 4 ++-- esphome/components/bl0942/bl0942.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp index e48715010c..c1cd48a1ac 100644 --- a/esphome/components/bl0906/bl0906.cpp +++ b/esphome/components/bl0906/bl0906.cpp @@ -97,10 +97,10 @@ void BL0906::handle_actions_() { return; } ActionCallbackFuncPtr ptr_func = nullptr; - for (int i = 0; i < this->action_queue_.size(); i++) { + for (size_t i = 0; i < this->action_queue_.size(); i++) { ptr_func = this->action_queue_[i]; if (ptr_func) { - ESP_LOGI(TAG, "HandleActionCallback[%d]", i); + ESP_LOGI(TAG, "HandleActionCallback[%zu]", i); (this->*ptr_func)(); } } diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp index 894fcbfbb7..95dd689b07 100644 --- a/esphome/components/bl0942/bl0942.cpp +++ b/esphome/components/bl0942/bl0942.cpp @@ -51,7 +51,7 @@ void BL0942::loop() { if (!avail) { return; } - if (avail < sizeof(buffer)) { + if (static_cast(avail) < sizeof(buffer)) { if (!this->rx_start_) { this->rx_start_ = millis(); } else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) { @@ -148,7 +148,7 @@ void BL0942::setup() { this->write_reg_(BL0942_REG_USR_WRPROT, 0); - if (this->read_reg_(BL0942_REG_MODE) != mode) + if (static_cast(this->read_reg_(BL0942_REG_MODE)) != mode) this->status_set_warning(LOG_STR("BL0942 setup failed!")); this->flush(); From 6880f9fc5c95eb96ba20c3fbbf335ef4567ddd59 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 09:09:10 -0500 Subject: [PATCH 65/99] [cm1106] Fix clang-tidy sign comparison error (#11045) --- esphome/components/cm1106/cm1106.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/cm1106/cm1106.cpp b/esphome/components/cm1106/cm1106.cpp index 339a1659ac..d88ea2e1da 100644 --- a/esphome/components/cm1106/cm1106.cpp +++ b/esphome/components/cm1106/cm1106.cpp @@ -13,7 +13,7 @@ static const uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03, uint8_t cm1106_checksum(const uint8_t *response, size_t len) { uint8_t crc = 0; - for (int i = 0; i < len - 1; i++) { + for (size_t i = 0; i < len - 1; i++) { crc -= response[i]; } return crc; From 94fea68e3e30fe12afe89f298af94f00010a6f78 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 09:12:40 -0500 Subject: [PATCH 66/99] [daikin_arc] Fix clang-tidy sign comparison errors (#11046) --- esphome/components/daikin_arc/daikin_arc.cpp | 31 ++++++++++++-------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/esphome/components/daikin_arc/daikin_arc.cpp b/esphome/components/daikin_arc/daikin_arc.cpp index f806463d00..068819ecd1 100644 --- a/esphome/components/daikin_arc/daikin_arc.cpp +++ b/esphome/components/daikin_arc/daikin_arc.cpp @@ -26,7 +26,7 @@ void DaikinArcClimate::transmit_query_() { uint8_t remote_header[8] = {0x11, 0xDA, 0x27, 0x00, 0x84, 0x87, 0x20, 0x00}; // Calculate checksum - for (int i = 0; i < sizeof(remote_header) - 1; i++) { + for (size_t i = 0; i < sizeof(remote_header) - 1; i++) { remote_header[sizeof(remote_header) - 1] += remote_header[i]; } @@ -102,7 +102,7 @@ void DaikinArcClimate::transmit_state() { remote_state[9] = fan_speed & 0xff; // Calculate checksum - for (int i = 0; i < sizeof(remote_header) - 1; i++) { + for (size_t i = 0; i < sizeof(remote_header) - 1; i++) { remote_header[sizeof(remote_header) - 1] += remote_header[i]; } @@ -350,7 +350,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { bool valid_daikin_frame = false; if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) { valid_daikin_frame = true; - int bytes_count = data.size() / 2 / 8; + size_t bytes_count = data.size() / 2 / 8; std::unique_ptr buf(new char[bytes_count * 3 + 1]); buf[0] = '\0'; for (size_t i = 0; i < bytes_count; i++) { @@ -370,7 +370,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { if (!valid_daikin_frame) { char sbuf[16 * 10 + 1]; sbuf[0] = '\0'; - for (size_t j = 0; j < data.size(); j++) { + for (size_t j = 0; j < static_cast(data.size()); j++) { if ((j - 2) % 16 == 0) { if (j > 0) { ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf); @@ -380,19 +380,26 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { char type_ch = ' '; // debug_tolerance = 25% - if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK)) <= data[j] && + data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK))) type_ch = 'P'; - if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE)) <= -data[j] && + -data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE))) type_ch = 'a'; - if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK)) <= data[j] && + data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK))) type_ch = 'H'; - if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE)) <= -data[j] && + -data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE))) type_ch = 'h'; - if (DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK)) <= data[j] && + data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK))) type_ch = 'B'; - if (DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE)) <= -data[j] && + -data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE))) type_ch = '1'; - if (DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE)) <= -data[j] && + -data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE))) type_ch = '0'; if (abs(data[j]) > 100000) { @@ -400,7 +407,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { } else { sprintf(sbuf, "%s%-5d[%c] ", sbuf, (int) (round(data[j] / 10.) * 10), type_ch); } - if (j == data.size() - 1) { + if (j + 1 == static_cast(data.size())) { ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf); } } From 3edcdc7d80a6d860e79f611b3e8724a7f5be9f07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 09:13:12 -0500 Subject: [PATCH 67/99] [es7210] Fix clang-tidy sign comparison errors (#11047) --- esphome/components/es7210/es7210.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/es7210/es7210.cpp b/esphome/components/es7210/es7210.cpp index e5729703ed..1358121c1b 100644 --- a/esphome/components/es7210/es7210.cpp +++ b/esphome/components/es7210/es7210.cpp @@ -97,12 +97,12 @@ bool ES7210::set_mic_gain(float mic_gain) { } bool ES7210::configure_sample_rate_() { - int mclk_fre = this->sample_rate_ * MCLK_DIV_FRE; + uint32_t mclk_fre = this->sample_rate_ * MCLK_DIV_FRE; int coeff = -1; - for (int i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) { + for (size_t i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) { if (ES7210_COEFFICIENTS[i].lrclk == this->sample_rate_ && ES7210_COEFFICIENTS[i].mclk == mclk_fre) - coeff = i; + coeff = static_cast(i); } if (coeff >= 0) { From 74c055745f7afdcb31022a8f74fd70cae02f04fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 09:14:24 -0500 Subject: [PATCH 68/99] [esp32_can] Fix clang-tidy sign comparison error (#11049) --- esphome/components/esp32_can/esp32_can.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_can/esp32_can.cpp b/esphome/components/esp32_can/esp32_can.cpp index 252482dc5e..cdef7b1930 100644 --- a/esphome/components/esp32_can/esp32_can.cpp +++ b/esphome/components/esp32_can/esp32_can.cpp @@ -68,7 +68,7 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config bool ESP32Can::setup_internal() { static int next_twai_ctrl_num = 0; - if (next_twai_ctrl_num >= SOC_TWAI_CONTROLLER_NUM) { + if (static_cast(next_twai_ctrl_num) >= SOC_TWAI_CONTROLLER_NUM) { ESP_LOGW(TAG, "Maximum number of esp32_can components created already"); this->mark_failed(); return false; From f86b83cda5be3e85a108bda16d32745e8ede80ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 09:15:56 -0500 Subject: [PATCH 69/99] [fingerprint_grow] Fix clang-tidy sign comparison error (#11050) --- esphome/components/fingerprint_grow/fingerprint_grow.cpp | 2 +- esphome/components/fingerprint_grow/fingerprint_grow.h | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index 54a267a404..eb7ede8fe9 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -80,7 +80,7 @@ void FingerprintGrowComponent::setup() { delay(20); // This delay guarantees the sensor will in fact be powered power. if (this->check_password_()) { - if (this->new_password_ != -1) { + if (this->new_password_ != std::numeric_limits::max()) { if (this->set_password_()) return; } else { diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 1c3098ef14..590c709c22 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -6,6 +6,7 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/uart/uart.h" +#include #include namespace esphome { @@ -177,7 +178,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF}; uint16_t capacity_ = 64; uint32_t password_ = 0x0; - uint32_t new_password_ = -1; + uint32_t new_password_ = std::numeric_limits::max(); GPIOPin *sensing_pin_{nullptr}; GPIOPin *sensor_power_pin_{nullptr}; uint8_t enrollment_image_ = 0; From 71be5a5f650c0db3d7a7a4cf4a8eb7db8a7e7463 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 09:48:39 -0500 Subject: [PATCH 70/99] [mixer] Fix clang-tidy sign comparison errors (#11061) --- esphome/components/mixer/speaker/mixer_speaker.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index fc0517c7be..b0b64f5709 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -572,7 +572,7 @@ void MixerSpeaker::audio_mixer_task(void *params) { } } else { // Determine how many frames to mix - for (int i = 0; i < transfer_buffers_with_data.size(); ++i) { + for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { const uint32_t frames_available_in_buffer = speakers_with_data[i]->get_audio_stream_info().bytes_to_frames(transfer_buffers_with_data[i]->available()); frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer); @@ -581,7 +581,7 @@ void MixerSpeaker::audio_mixer_task(void *params) { audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info(); // Mix two streams together - for (int i = 1; i < transfer_buffers_with_data.size(); ++i) { + for (size_t i = 1; i < transfer_buffers_with_data.size(); ++i) { mix_audio_samples(primary_buffer, primary_stream_info, reinterpret_cast(transfer_buffers_with_data[i]->get_buffer_start()), speakers_with_data[i]->get_audio_stream_info(), @@ -596,7 +596,7 @@ void MixerSpeaker::audio_mixer_task(void *params) { } // Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks - for (int i = 0; i < transfer_buffers_with_data.size(); ++i) { + for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { transfer_buffers_with_data[i]->decrease_buffer_length( speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix)); speakers_with_data[i]->pending_playback_frames_ += frames_to_mix; From 192856e8d13e16c793639656feed7f4572fc2f43 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 09:52:47 -0500 Subject: [PATCH 71/99] [nau7802] Fix clang-tidy sign comparison errors (#11062) --- esphome/components/nau7802/nau7802.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/nau7802/nau7802.cpp b/esphome/components/nau7802/nau7802.cpp index acdca03fdb..6a31b754f7 100644 --- a/esphome/components/nau7802/nau7802.cpp +++ b/esphome/components/nau7802/nau7802.cpp @@ -218,7 +218,7 @@ void NAU7802Sensor::dump_config() { void NAU7802Sensor::write_value_(uint8_t start_reg, size_t size, int32_t value) { uint8_t data[4]; - for (int i = 0; i < size; i++) { + for (size_t i = 0; i < size; i++) { data[i] = 0xFF & (value >> (size - 1 - i) * 8); } this->write_register(start_reg, data, size); @@ -228,7 +228,7 @@ int32_t NAU7802Sensor::read_value_(uint8_t start_reg, size_t size) { uint8_t data[4]; this->read_register(start_reg, data, size); int32_t result = 0; - for (int i = 0; i < size; i++) { + for (size_t i = 0; i < size; i++) { result |= data[i] << (size - 1 - i) * 8; } // extend sign bit From 1635767aa2634ed9f3b0972e87ca72b8c59c1e79 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 09:56:44 -0500 Subject: [PATCH 72/99] [max7219digit] Fix clang-tidy sign comparison error (#11060) --- esphome/components/max7219digit/max7219digit.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index 9b9921d2f0..6df3c4d7c8 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -90,7 +90,7 @@ void MAX7219Component::loop() { } if (this->scroll_mode_ == ScrollMode::STOP) { - if (this->stepsleft_ + get_width_internal() == first_line_size + 1) { + if (static_cast(this->stepsleft_ + get_width_internal()) == first_line_size + 1) { if (millis_since_last_scroll < this->scroll_dwell_) { ESP_LOGVV(TAG, "Dwell time at end of string in case of stop at end. Step %d, since last scroll %d, dwell %d.", this->stepsleft_, millis_since_last_scroll, this->scroll_dwell_); From 2a8796437d4819dd1de3d5727ce586b1deb32765 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 09:59:02 -0500 Subject: [PATCH 73/99] [ltr_als_ps] Fix clang-tidy sign comparison errors (#11058) --- esphome/components/ltr_als_ps/ltr_als_ps.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.cpp b/esphome/components/ltr_als_ps/ltr_als_ps.cpp index bf27c01e26..c3ea5848c8 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.cpp +++ b/esphome/components/ltr_als_ps/ltr_als_ps.cpp @@ -2,6 +2,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include using esphome::i2c::ErrorCode; @@ -14,30 +15,30 @@ static const uint8_t MAX_TRIES = 5; template T get_next(const T (&array)[size], const T val) { size_t i = 0; - size_t idx = -1; - while (idx == -1 && i < size) { + size_t idx = std::numeric_limits::max(); + while (idx == std::numeric_limits::max() && i < size) { if (array[i] == val) { idx = i; break; } i++; } - if (idx == -1 || i + 1 >= size) + if (idx == std::numeric_limits::max() || i + 1 >= size) return val; return array[i + 1]; } template T get_prev(const T (&array)[size], const T val) { size_t i = size - 1; - size_t idx = -1; - while (idx == -1 && i > 0) { + size_t idx = std::numeric_limits::max(); + while (idx == std::numeric_limits::max() && i > 0) { if (array[i] == val) { idx = i; break; } i--; } - if (idx == -1 || i == 0) + if (idx == std::numeric_limits::max() || i == 0) return val; return array[i - 1]; } From 5e7f5bf890da8b06e3ce8e9f488da41a41b8b3b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 09:59:36 -0500 Subject: [PATCH 74/99] [ltr501] Fix clang-tidy sign comparison errors (#11057) --- esphome/components/ltr501/ltr501.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/esphome/components/ltr501/ltr501.cpp b/esphome/components/ltr501/ltr501.cpp index b249d23666..be5a4ddccf 100644 --- a/esphome/components/ltr501/ltr501.cpp +++ b/esphome/components/ltr501/ltr501.cpp @@ -2,6 +2,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include using esphome::i2c::ErrorCode; @@ -28,30 +29,30 @@ bool operator!=(const GainTimePair &lhs, const GainTimePair &rhs) { template T get_next(const T (&array)[size], const T val) { size_t i = 0; - size_t idx = -1; - while (idx == -1 && i < size) { + size_t idx = std::numeric_limits::max(); + while (idx == std::numeric_limits::max() && i < size) { if (array[i] == val) { idx = i; break; } i++; } - if (idx == -1 || i + 1 >= size) + if (idx == std::numeric_limits::max() || i + 1 >= size) return val; return array[i + 1]; } template T get_prev(const T (&array)[size], const T val) { size_t i = size - 1; - size_t idx = -1; - while (idx == -1 && i > 0) { + size_t idx = std::numeric_limits::max(); + while (idx == std::numeric_limits::max() && i > 0) { if (array[i] == val) { idx = i; break; } i--; } - if (idx == -1 || i == 0) + if (idx == std::numeric_limits::max() || i == 0) return val; return array[i - 1]; } From 9384f0683b6bbe770c089790900679db7a4757df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 10:02:02 -0500 Subject: [PATCH 75/99] [kamstrup_kmp] Fix clang-tidy sign comparison errors (#11055) --- esphome/components/kamstrup_kmp/kamstrup_kmp.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp index c058c7b3aa..e5fa035682 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -22,7 +22,7 @@ void KamstrupKMPComponent::dump_config() { LOG_SENSOR(" ", "Flow", this->flow_sensor_); LOG_SENSOR(" ", "Volume", this->volume_sensor_); - for (int i = 0; i < this->custom_sensors_.size(); i++) { + for (size_t i = 0; i < this->custom_sensors_.size(); i++) { LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]); ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[i]); } @@ -268,7 +268,7 @@ void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint } // Custom sensors - for (int i = 0; i < this->custom_commands_.size(); i++) { + for (size_t i = 0; i < this->custom_commands_.size(); i++) { if (command == this->custom_commands_[i]) { this->custom_sensors_[i]->publish_state(value); } From 646508006c382e2bb3dbbebda6fcd9a51a1b4e44 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 10:02:54 -0500 Subject: [PATCH 76/99] [ili9xxx] Fix clang-tidy sign comparison errors (#11054) --- esphome/components/ili9xxx/ili9xxx_display.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index ec0a860aa8..2a3d0edca7 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -325,7 +325,7 @@ void ILI9XXXDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, cons // we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't bother this->write_array(ptr, w * h * 2); } else { - for (size_t y = 0; y != h; y++) { + for (size_t y = 0; y != static_cast(h); y++) { this->write_array(ptr + (y + y_offset) * stride + x_offset, w * 2); } } @@ -349,7 +349,7 @@ void ILI9XXXDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, cons App.feed_wdt(); } // end of line? Skip to the next. - if (++pixel == w) { + if (++pixel == static_cast(w)) { pixel = 0; ptr += (x_pad + x_offset) * 2; } From 4c8fc5f4e662dc85aa430fe0067db602f97dcce6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 10:20:59 -0500 Subject: [PATCH 77/99] [pid] Fix clang-tidy sign comparison error (#11063) --- esphome/components/pid/pid_controller.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/pid/pid_controller.cpp b/esphome/components/pid/pid_controller.cpp index 1a16f14542..5d7aecdb05 100644 --- a/esphome/components/pid/pid_controller.cpp +++ b/esphome/components/pid/pid_controller.cpp @@ -104,7 +104,7 @@ float PIDController::weighted_average_(std::deque &list, float new_value, list.push_front(new_value); // keep only 'samples' readings, by popping off the back of the list - while (list.size() > samples) + while (samples > 0 && list.size() > static_cast(samples)) list.pop_back(); // calculate and return the average of all values in the list From e55df1babc2016f329ea2c3552d5e5cb78106ce0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 10:34:18 -0500 Subject: [PATCH 78/99] [key_collector] Fix clang-tidy sign comparison errors (#11056) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/key_collector/key_collector.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/key_collector/key_collector.h b/esphome/components/key_collector/key_collector.h index 6e585ddd8e..35e8141ce5 100644 --- a/esphome/components/key_collector/key_collector.h +++ b/esphome/components/key_collector/key_collector.h @@ -13,8 +13,8 @@ class KeyCollector : public Component { void loop() override; void dump_config() override; void set_provider(key_provider::KeyProvider *provider); - void set_min_length(int min_length) { this->min_length_ = min_length; }; - void set_max_length(int max_length) { this->max_length_ = max_length; }; + void set_min_length(uint32_t min_length) { this->min_length_ = min_length; }; + void set_max_length(uint32_t max_length) { this->max_length_ = max_length; }; void set_start_keys(std::string start_keys) { this->start_keys_ = std::move(start_keys); }; void set_end_keys(std::string end_keys) { this->end_keys_ = std::move(end_keys); }; void set_end_key_required(bool end_key_required) { this->end_key_required_ = end_key_required; }; @@ -33,8 +33,8 @@ class KeyCollector : public Component { protected: void key_pressed_(uint8_t key); - int min_length_{0}; - int max_length_{0}; + uint32_t min_length_{0}; + uint32_t max_length_{0}; std::string start_keys_; std::string end_keys_; bool end_key_required_{false}; From 7147479f9079826c4f67928019ab9625cb540e56 Mon Sep 17 00:00:00 2001 From: Mort <96076810+mortification77@users.noreply.github.com> Date: Mon, 6 Oct 2025 11:48:05 -0400 Subject: [PATCH 79/99] [qmc5883l] Added drdy_pin option to allow it to run max rate (#10901) Co-authored-by: Lamer Mortification Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/qmc5883l/qmc5883l.cpp | 15 +++++++ esphome/components/qmc5883l/qmc5883l.h | 3 ++ esphome/components/qmc5883l/sensor.py | 43 +++++++++++++------ tests/components/qmc5883l/common.yaml | 2 + tests/components/qmc5883l/test.esp32-ard.yaml | 1 + .../qmc5883l/test.esp32-c3-ard.yaml | 1 + .../qmc5883l/test.esp32-c3-idf.yaml | 1 + tests/components/qmc5883l/test.esp32-idf.yaml | 1 + .../components/qmc5883l/test.esp8266-ard.yaml | 1 + .../components/qmc5883l/test.rp2040-ard.yaml | 1 + 10 files changed, 57 insertions(+), 12 deletions(-) diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index c9196f2469..d2041a2d52 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -8,6 +8,7 @@ namespace esphome { namespace qmc5883l { static const char *const TAG = "qmc5883l"; + static const uint8_t QMC5883L_ADDRESS = 0x0D; static const uint8_t QMC5883L_REGISTER_DATA_X_LSB = 0x00; @@ -32,6 +33,10 @@ void QMC5883LComponent::setup() { } delay(10); + if (this->drdy_pin_) { + this->drdy_pin_->setup(); + } + uint8_t control_1 = 0; control_1 |= 0b01 << 0; // MODE (Mode) -> 0b00=standby, 0b01=continuous control_1 |= this->datarate_ << 2; @@ -64,6 +69,7 @@ void QMC5883LComponent::setup() { high_freq_.start(); } } + void QMC5883LComponent::dump_config() { ESP_LOGCONFIG(TAG, "QMC5883L:"); LOG_I2C_DEVICE(this); @@ -77,11 +83,20 @@ void QMC5883LComponent::dump_config() { LOG_SENSOR(" ", "Z Axis", this->z_sensor_); LOG_SENSOR(" ", "Heading", this->heading_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_PIN(" DRDY Pin: ", this->drdy_pin_); } + float QMC5883LComponent::get_setup_priority() const { return setup_priority::DATA; } + void QMC5883LComponent::update() { i2c::ErrorCode err; uint8_t status = false; + + // If DRDY pin is configured and the data is not ready return. + if (this->drdy_pin_ && !this->drdy_pin_->digital_read()) { + return; + } + // Status byte gets cleared when data is read, so we have to read this first. // If status and two axes are desired, it's possible to save one byte of traffic by enabling // ROL_PNT in setup and reading 7 bytes starting at the status register. diff --git a/esphome/components/qmc5883l/qmc5883l.h b/esphome/components/qmc5883l/qmc5883l.h index 3202e37780..5ba7180e23 100644 --- a/esphome/components/qmc5883l/qmc5883l.h +++ b/esphome/components/qmc5883l/qmc5883l.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/core/hal.h" namespace esphome { namespace qmc5883l { @@ -33,6 +34,7 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice { float get_setup_priority() const override; void update() override; + void set_drdy_pin(GPIOPin *pin) { drdy_pin_ = pin; } void set_datarate(QMC5883LDatarate datarate) { datarate_ = datarate; } void set_range(QMC5883LRange range) { range_ = range; } void set_oversampling(QMC5883LOversampling oversampling) { oversampling_ = oversampling; } @@ -51,6 +53,7 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *z_sensor_{nullptr}; sensor::Sensor *heading_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; + GPIOPin *drdy_pin_{nullptr}; enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, diff --git a/esphome/components/qmc5883l/sensor.py b/esphome/components/qmc5883l/sensor.py index ade286cb9e..b79e370a05 100644 --- a/esphome/components/qmc5883l/sensor.py +++ b/esphome/components/qmc5883l/sensor.py @@ -1,8 +1,12 @@ +import logging + +from esphome import pins import esphome.codegen as cg from esphome.components import i2c, sensor import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, + CONF_DATA_RATE, CONF_FIELD_STRENGTH_X, CONF_FIELD_STRENGTH_Y, CONF_FIELD_STRENGTH_Z, @@ -21,6 +25,10 @@ from esphome.const import ( UNIT_MICROTESLA, ) +_LOGGER = logging.getLogger(__name__) + +CONF_DRDY_PIN = "drdy_pin" + DEPENDENCIES = ["i2c"] qmc5883l_ns = cg.esphome_ns.namespace("qmc5883l") @@ -52,6 +60,18 @@ QMC5883LOversamplings = { } +def validate_config(config): + if ( + config[CONF_UPDATE_INTERVAL].total_milliseconds < 15 + and CONF_DRDY_PIN not in config + ): + _LOGGER.warning( + "[qmc5883l] 'update_interval' is less than 15ms and 'drdy_pin' is " + "not configured, this may result in I2C errors" + ) + return config + + def validate_enum(enum_values, units=None, int=True): _units = [] if units is not None: @@ -88,7 +108,7 @@ temperature_schema = sensor.sensor_schema( state_class=STATE_CLASS_MEASUREMENT, ) -CONFIG_SCHEMA = ( +CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(QMC5883LComponent), @@ -104,29 +124,25 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_FIELD_STRENGTH_Z): field_strength_schema, cv.Optional(CONF_HEADING): heading_schema, cv.Optional(CONF_TEMPERATURE): temperature_schema, + cv.Optional(CONF_DRDY_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_DATA_RATE, default="200hz"): validate_enum( + QMC5883LDatarates, units=["hz", "Hz"] + ), } ) .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x0D)) + .extend(i2c.i2c_device_schema(0x0D)), + validate_config, ) -def auto_data_rate(config): - interval_sec = config[CONF_UPDATE_INTERVAL].total_milliseconds / 1000 - interval_hz = 1.0 / interval_sec - for datarate in sorted(QMC5883LDatarates.keys()): - if float(datarate) >= interval_hz: - return QMC5883LDatarates[datarate] - return QMC5883LDatarates[200] - - 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_oversampling(config[CONF_OVERSAMPLING])) - cg.add(var.set_datarate(auto_data_rate(config))) + cg.add(var.set_datarate(config[CONF_DATA_RATE])) cg.add(var.set_range(config[CONF_RANGE])) if CONF_FIELD_STRENGTH_X in config: sens = await sensor.new_sensor(config[CONF_FIELD_STRENGTH_X]) @@ -143,3 +159,6 @@ async def to_code(config): if CONF_TEMPERATURE in config: sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) + if CONF_DRDY_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_DRDY_PIN]) + cg.add(var.set_drdy_pin(pin)) diff --git a/tests/components/qmc5883l/common.yaml b/tests/components/qmc5883l/common.yaml index 5d8ac73b4f..c8ad4ba006 100644 --- a/tests/components/qmc5883l/common.yaml +++ b/tests/components/qmc5883l/common.yaml @@ -17,5 +17,7 @@ sensor: temperature: name: QMC5883L Temperature range: 800uT + data_rate: 200Hz oversampling: 256x update_interval: 15s + drdy_pin: ${drdy_pin} diff --git a/tests/components/qmc5883l/test.esp32-ard.yaml b/tests/components/qmc5883l/test.esp32-ard.yaml index 63c3bd6afd..2cf2041501 100644 --- a/tests/components/qmc5883l/test.esp32-ard.yaml +++ b/tests/components/qmc5883l/test.esp32-ard.yaml @@ -1,5 +1,6 @@ substitutions: scl_pin: GPIO16 sda_pin: GPIO17 + drdy_pin: GPIO18 <<: !include common.yaml diff --git a/tests/components/qmc5883l/test.esp32-c3-ard.yaml b/tests/components/qmc5883l/test.esp32-c3-ard.yaml index ee2c29ca4e..677501d15a 100644 --- a/tests/components/qmc5883l/test.esp32-c3-ard.yaml +++ b/tests/components/qmc5883l/test.esp32-c3-ard.yaml @@ -1,5 +1,6 @@ substitutions: scl_pin: GPIO5 sda_pin: GPIO4 + drdy_pin: GPIO6 <<: !include common.yaml diff --git a/tests/components/qmc5883l/test.esp32-c3-idf.yaml b/tests/components/qmc5883l/test.esp32-c3-idf.yaml index ee2c29ca4e..677501d15a 100644 --- a/tests/components/qmc5883l/test.esp32-c3-idf.yaml +++ b/tests/components/qmc5883l/test.esp32-c3-idf.yaml @@ -1,5 +1,6 @@ substitutions: scl_pin: GPIO5 sda_pin: GPIO4 + drdy_pin: GPIO6 <<: !include common.yaml diff --git a/tests/components/qmc5883l/test.esp32-idf.yaml b/tests/components/qmc5883l/test.esp32-idf.yaml index 63c3bd6afd..2cf2041501 100644 --- a/tests/components/qmc5883l/test.esp32-idf.yaml +++ b/tests/components/qmc5883l/test.esp32-idf.yaml @@ -1,5 +1,6 @@ substitutions: scl_pin: GPIO16 sda_pin: GPIO17 + drdy_pin: GPIO18 <<: !include common.yaml diff --git a/tests/components/qmc5883l/test.esp8266-ard.yaml b/tests/components/qmc5883l/test.esp8266-ard.yaml index ee2c29ca4e..65b0fd75d9 100644 --- a/tests/components/qmc5883l/test.esp8266-ard.yaml +++ b/tests/components/qmc5883l/test.esp8266-ard.yaml @@ -1,5 +1,6 @@ substitutions: scl_pin: GPIO5 sda_pin: GPIO4 + drdy_pin: GPIO2 <<: !include common.yaml diff --git a/tests/components/qmc5883l/test.rp2040-ard.yaml b/tests/components/qmc5883l/test.rp2040-ard.yaml index ee2c29ca4e..65b0fd75d9 100644 --- a/tests/components/qmc5883l/test.rp2040-ard.yaml +++ b/tests/components/qmc5883l/test.rp2040-ard.yaml @@ -1,5 +1,6 @@ substitutions: scl_pin: GPIO5 sda_pin: GPIO4 + drdy_pin: GPIO2 <<: !include common.yaml From 22e06ba063ccfdd123f8f03a761aadd4be059cf6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 10:53:24 -0500 Subject: [PATCH 80/99] [matrix_keypad] Fix clang-tidy sign comparison error (#11059) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/matrix_keypad/matrix_keypad.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/matrix_keypad/matrix_keypad.h b/esphome/components/matrix_keypad/matrix_keypad.h index 8b309b42c2..258ab4fadc 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.h +++ b/esphome/components/matrix_keypad/matrix_keypad.h @@ -29,9 +29,9 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component { void set_columns(std::vector pins) { columns_ = std::move(pins); }; void set_rows(std::vector pins) { rows_ = std::move(pins); }; void set_keys(std::string keys) { keys_ = std::move(keys); }; - void set_debounce_time(int debounce_time) { debounce_time_ = debounce_time; }; - void set_has_diodes(int has_diodes) { has_diodes_ = has_diodes; }; - void set_has_pulldowns(int has_pulldowns) { has_pulldowns_ = has_pulldowns; }; + void set_debounce_time(uint32_t debounce_time) { debounce_time_ = debounce_time; }; + void set_has_diodes(bool has_diodes) { has_diodes_ = has_diodes; }; + void set_has_pulldowns(bool has_pulldowns) { has_pulldowns_ = has_pulldowns; }; void register_listener(MatrixKeypadListener *listener); void register_key_trigger(MatrixKeyTrigger *trig); @@ -40,7 +40,7 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component { std::vector rows_; std::vector columns_; std::string keys_; - int debounce_time_ = 0; + uint32_t debounce_time_ = 0; bool has_diodes_{false}; bool has_pulldowns_{false}; int pressed_key_ = -1; From eb16d322cd9d56a77c08367c8aebf24b4274fb50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 11:07:44 -0500 Subject: [PATCH 81/99] [audio, i2s_audio] Fix clang-tidy sign comparison errors (#11044) --- esphome/components/audio/audio.cpp | 2 +- esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/audio.cpp b/esphome/components/audio/audio.cpp index 2a58c38ac7..9cc9b7d0da 100644 --- a/esphome/components/audio/audio.cpp +++ b/esphome/components/audio/audio.cpp @@ -57,7 +57,7 @@ const char *audio_file_type_to_string(AudioFileType file_type) { void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor, size_t samples_to_scale) { // Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same. - for (int i = 0; i < samples_to_scale; i++) { + for (size_t i = 0; i < samples_to_scale; i++) { int32_t acc = (int32_t) audio_samples[i] * (int32_t) scale_factor; output_buffer[i] = (int16_t) (acc >> 15); } diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 7ae3ec8b3b..53e378c41e 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -377,7 +377,7 @@ void I2SAudioSpeaker::speaker_task(void *params) { this_speaker->current_stream_info_.get_bits_per_sample() <= 16) { size_t len = bytes_read / sizeof(int16_t); int16_t *tmp_buf = (int16_t *) new_data; - for (int i = 0; i < len; i += 2) { + for (size_t i = 0; i < len; i += 2) { int16_t tmp = tmp_buf[i]; tmp_buf[i] = tmp_buf[i + 1]; tmp_buf[i + 1] = tmp; From 8cfb6578d1df9c78bdb5d254abe6a5904e867811 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 11:10:54 -0500 Subject: [PATCH 82/99] [graphical_display_menu] Fix clang-tidy sign comparison errors (#11052) --- .../graphical_display_menu/graphical_display_menu.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.cpp b/esphome/components/graphical_display_menu/graphical_display_menu.cpp index 1a29536b46..2b120a746f 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.cpp +++ b/esphome/components/graphical_display_menu/graphical_display_menu.cpp @@ -116,7 +116,7 @@ void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const int number_items_fit_to_screen = 0; const int max_item_index = this->displayed_item_->items_size() - 1; - for (size_t i = 0; i <= max_item_index; i++) { + for (size_t i = 0; max_item_index >= 0 && i <= static_cast(max_item_index); i++) { const auto *item = this->displayed_item_->get_item(i); const bool selected = i == this->cursor_index_; const display::Rect item_dimensions = this->measure_item(display, item, bounds, selected); @@ -174,7 +174,8 @@ void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const display->filled_rectangle(bounds->x, bounds->y, max_width, total_height, this->background_color_); auto y_offset = bounds->y; - for (size_t i = first_item_index; i <= last_item_index; i++) { + for (size_t i = static_cast(first_item_index); + last_item_index >= 0 && i <= static_cast(last_item_index); i++) { const auto *item = this->displayed_item_->get_item(i); const bool selected = i == this->cursor_index_; display::Rect dimensions = menu_dimensions[i]; From a88182c8e33862f58fc5fbd725d2ac105ebffd86 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 11:12:39 -0500 Subject: [PATCH 83/99] [statsd] Fix clang-tidy sign comparison error (#11069) --- esphome/components/statsd/statsd.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/statsd/statsd.cpp b/esphome/components/statsd/statsd.cpp index 05f71c7b24..7729f36858 100644 --- a/esphome/components/statsd/statsd.cpp +++ b/esphome/components/statsd/statsd.cpp @@ -151,7 +151,7 @@ void StatsdComponent::send_(std::string *out) { int n_bytes = this->sock_->sendto(out->c_str(), out->length(), 0, reinterpret_cast(&this->destination_), sizeof(this->destination_)); - if (n_bytes != out->length()) { + if (n_bytes != static_cast(out->length())) { ESP_LOGE(TAG, "Failed to send UDP packed (%d of %d)", n_bytes, out->length()); } #endif From 697cab45dd1df53e0688b12d9c556f1b438e9c9e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 7 Oct 2025 05:19:31 +1300 Subject: [PATCH 84/99] [json] Add `parse_json` overload for `const char *` (#11039) Co-authored-by: J. Nick Koston --- esphome/components/json/json_util.cpp | 10 +++++++--- esphome/components/json/json_util.h | 7 ++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 643f23f499..dbdf6e3486 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -19,15 +19,19 @@ std::string build_json(const json_build_t &f) { bool parse_json(const std::string &data, const json_parse_t &f) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - JsonDocument doc = parse_json(data); + JsonDocument doc = parse_json(reinterpret_cast(data.c_str()), data.size()); if (doc.overflowed() || doc.isNull()) return false; return f(doc.as()); // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } -JsonDocument parse_json(const std::string &data) { +JsonDocument parse_json(const uint8_t *data, size_t len) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + if (data == nullptr || len == 0) { + ESP_LOGE(TAG, "No data to parse"); + return JsonObject(); // return unbound object + } #ifdef USE_PSRAM auto doc_allocator = SpiRamAllocator(); JsonDocument json_document(&doc_allocator); @@ -38,7 +42,7 @@ JsonDocument parse_json(const std::string &data) { ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); return JsonObject(); // return unbound object } - DeserializationError err = deserializeJson(json_document, data); + DeserializationError err = deserializeJson(json_document, data, len); if (err == DeserializationError::Ok) { return json_document; diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index a8f452d7d0..91cc84dc14 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -50,8 +50,13 @@ std::string build_json(const json_build_t &f); /// Parse a JSON string and run the provided json parse function if it's valid. bool parse_json(const std::string &data, const json_parse_t &f); + /// Parse a JSON string and return the root JsonDocument (or an unbound object on error) -JsonDocument parse_json(const std::string &data); +JsonDocument parse_json(const uint8_t *data, size_t len); +/// Parse a JSON string and return the root JsonDocument (or an unbound object on error) +inline JsonDocument parse_json(const std::string &data) { + return parse_json(reinterpret_cast(data.c_str()), data.size()); +} /// Builder class for creating JSON documents without lambdas class JsonBuilder { From b532e04ae4fafb61c3ecee3b8f632c244c6231dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 11:24:52 -0500 Subject: [PATCH 85/99] [st7789v] Fix clang-tidy sign comparison errors (#11068) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/st7789v/st7789v.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp index 44f2293ac4..ade9c1126f 100644 --- a/esphome/components/st7789v/st7789v.cpp +++ b/esphome/components/st7789v/st7789v.cpp @@ -176,8 +176,9 @@ void ST7789V::write_display_data() { if (this->eightbitcolor_) { uint8_t temp_buffer[TEMP_BUFFER_SIZE]; size_t temp_index = 0; - for (int line = 0; line < this->get_buffer_length_(); line = line + this->get_width_internal()) { - for (int index = 0; index < this->get_width_internal(); ++index) { + size_t width = static_cast(this->get_width_internal()); + for (size_t line = 0; line < this->get_buffer_length_(); line += width) { + for (size_t index = 0; index < width; ++index) { auto color = display::ColorUtil::color_to_565( display::ColorUtil::to_color(this->buffer_[index + line], display::ColorOrder::COLOR_ORDER_RGB, display::ColorBitness::COLOR_BITNESS_332, true)); From 3f4250fcd713dd97afe7474778b7e3613ef7ae76 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 11:27:34 -0500 Subject: [PATCH 86/99] [st7567_i2c] Fix clang-tidy sign comparison warning (#11067) --- esphome/components/st7567_i2c/st7567_i2c.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/st7567_i2c/st7567_i2c.cpp b/esphome/components/st7567_i2c/st7567_i2c.cpp index 710e473b11..14c21d5148 100644 --- a/esphome/components/st7567_i2c/st7567_i2c.cpp +++ b/esphome/components/st7567_i2c/st7567_i2c.cpp @@ -50,8 +50,10 @@ void HOT I2CST7567::write_display_data() { static const size_t BLOCK_SIZE = 64; for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x += BLOCK_SIZE) { + size_t remaining = static_cast(this->get_width_internal()) - x; + size_t chunk = remaining > BLOCK_SIZE ? BLOCK_SIZE : remaining; this->write_register(esphome::st7567_base::ST7567_SET_START_LINE, &buffer_[y * this->get_width_internal() + x], - this->get_width_internal() - x > BLOCK_SIZE ? BLOCK_SIZE : this->get_width_internal() - x); + chunk); } } } From a7f556c25f2f0cbb00b1447d1e735d7d3f63f32c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 11:28:41 -0500 Subject: [PATCH 87/99] [esp32_ble] Fix clang-tidy sign comparison error (#11048) --- esphome/components/esp32_ble/ble_advertising.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp index df70768c23..68704e49e2 100644 --- a/esphome/components/esp32_ble/ble_advertising.cpp +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -152,7 +152,7 @@ void BLEAdvertising::loop() { if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) { this->stop(); this->current_adv_index_ += 1; - if (this->current_adv_index_ >= this->raw_advertisements_callbacks_.size()) { + if (static_cast(this->current_adv_index_) >= this->raw_advertisements_callbacks_.size()) { this->current_adv_index_ = -1; } this->start(); From 8d4b347e5ca641cc8ce0e1723003cb02581b7d37 Mon Sep 17 00:00:00 2001 From: Beormund <75735592+Beormund@users.noreply.github.com> Date: Mon, 6 Oct 2025 17:36:33 +0100 Subject: [PATCH 88/99] [lm75b] Add LM75B temperature sensor component (#10534) --- CODEOWNERS | 1 + esphome/components/lm75b/__init__.py | 0 esphome/components/lm75b/lm75b.cpp | 39 +++++++++++++++++++ esphome/components/lm75b/lm75b.h | 19 +++++++++ esphome/components/lm75b/sensor.py | 34 ++++++++++++++++ tests/components/lm75b/common.yaml | 9 +++++ tests/components/lm75b/test.esp32-ard.yaml | 5 +++ tests/components/lm75b/test.esp32-c3-ard.yaml | 5 +++ tests/components/lm75b/test.esp32-c3-idf.yaml | 5 +++ tests/components/lm75b/test.esp32-idf.yaml | 5 +++ tests/components/lm75b/test.esp8266-ard.yaml | 5 +++ tests/components/lm75b/test.rp2040-ard.yaml | 5 +++ 12 files changed, 132 insertions(+) create mode 100644 esphome/components/lm75b/__init__.py create mode 100644 esphome/components/lm75b/lm75b.cpp create mode 100644 esphome/components/lm75b/lm75b.h create mode 100644 esphome/components/lm75b/sensor.py create mode 100644 tests/components/lm75b/common.yaml create mode 100644 tests/components/lm75b/test.esp32-ard.yaml create mode 100644 tests/components/lm75b/test.esp32-c3-ard.yaml create mode 100644 tests/components/lm75b/test.esp32-c3-idf.yaml create mode 100644 tests/components/lm75b/test.esp32-idf.yaml create mode 100644 tests/components/lm75b/test.esp8266-ard.yaml create mode 100644 tests/components/lm75b/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 0b9935faf7..d5b81d548e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -256,6 +256,7 @@ esphome/components/libretiny_pwm/* @kuba2k2 esphome/components/light/* @esphome/core esphome/components/lightwaverf/* @max246 esphome/components/lilygo_t5_47/touchscreen/* @jesserockz +esphome/components/lm75b/* @beormund esphome/components/ln882x/* @lamauny esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core diff --git a/esphome/components/lm75b/__init__.py b/esphome/components/lm75b/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/lm75b/lm75b.cpp b/esphome/components/lm75b/lm75b.cpp new file mode 100644 index 0000000000..19398eda85 --- /dev/null +++ b/esphome/components/lm75b/lm75b.cpp @@ -0,0 +1,39 @@ +#include "lm75b.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace lm75b { + +static const char *const TAG = "lm75b"; + +void LM75BComponent::dump_config() { + ESP_LOGCONFIG(TAG, "LM75B:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Setting up LM75B failed!"); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this); +} + +void LM75BComponent::update() { + // Create a temporary buffer + uint8_t buff[2]; + if (this->read_register(LM75B_REG_TEMPERATURE, buff, 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + // Obtain combined 16-bit value + int16_t raw_temperature = (buff[0] << 8) | buff[1]; + // Read the 11-bit raw temperature value + raw_temperature >>= 5; + // Publish the temperature in °C + this->publish_state(raw_temperature * 0.125); + if (this->status_has_warning()) { + this->status_clear_warning(); + } +} + +} // namespace lm75b +} // namespace esphome diff --git a/esphome/components/lm75b/lm75b.h b/esphome/components/lm75b/lm75b.h new file mode 100644 index 0000000000..79d9fa3f32 --- /dev/null +++ b/esphome/components/lm75b/lm75b.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace lm75b { + +static const uint8_t LM75B_REG_TEMPERATURE = 0x00; + +class LM75BComponent : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { + public: + void dump_config() override; + void update() override; +}; + +} // namespace lm75b +} // namespace esphome diff --git a/esphome/components/lm75b/sensor.py b/esphome/components/lm75b/sensor.py new file mode 100644 index 0000000000..335446b62f --- /dev/null +++ b/esphome/components/lm75b/sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +CODEOWNERS = ["@beormund"] +DEPENDENCIES = ["i2c"] + +lm75b_ns = cg.esphome_ns.namespace("lm75b") +LM75BComponent = lm75b_ns.class_( + "LM75BComponent", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + LM75BComponent, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x48)) +) + + +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/lm75b/common.yaml b/tests/components/lm75b/common.yaml new file mode 100644 index 0000000000..e451c2f679 --- /dev/null +++ b/tests/components/lm75b/common.yaml @@ -0,0 +1,9 @@ +i2c: + - id: i2c_lm75b + scl: ${scl_pin} + sda: ${sda_pin} + +sensor: + - platform: lm75b + name: LM75B Temperature + update_interval: 30s diff --git a/tests/components/lm75b/test.esp32-ard.yaml b/tests/components/lm75b/test.esp32-ard.yaml new file mode 100644 index 0000000000..43264df633 --- /dev/null +++ b/tests/components/lm75b/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO15 + sda_pin: GPIO13 + +<<: !include common.yaml diff --git a/tests/components/lm75b/test.esp32-c3-ard.yaml b/tests/components/lm75b/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/lm75b/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/lm75b/test.esp32-c3-idf.yaml b/tests/components/lm75b/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/lm75b/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/lm75b/test.esp32-idf.yaml b/tests/components/lm75b/test.esp32-idf.yaml new file mode 100644 index 0000000000..43264df633 --- /dev/null +++ b/tests/components/lm75b/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO15 + sda_pin: GPIO13 + +<<: !include common.yaml diff --git a/tests/components/lm75b/test.esp8266-ard.yaml b/tests/components/lm75b/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/lm75b/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/lm75b/test.rp2040-ard.yaml b/tests/components/lm75b/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/lm75b/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml From abeadc783025cb3f605df6219934ca549705868d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 11:49:50 -0500 Subject: [PATCH 89/99] [remote_base] Fix clang-tidy sign comparison error (#11064) --- esphome/components/remote_base/gobox_protocol.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/remote_base/gobox_protocol.cpp b/esphome/components/remote_base/gobox_protocol.cpp index 54e0dff663..4f6de5e59e 100644 --- a/esphome/components/remote_base/gobox_protocol.cpp +++ b/esphome/components/remote_base/gobox_protocol.cpp @@ -10,8 +10,8 @@ constexpr uint32_t BIT_MARK_US = 580; // 70us seems like a safe time delta for constexpr uint32_t BIT_ONE_SPACE_US = 1640; constexpr uint32_t BIT_ZERO_SPACE_US = 545; constexpr uint64_t HEADER = 0b011001001100010uL; // 15 bits -constexpr uint64_t HEADER_SIZE = 15; -constexpr uint64_t CODE_SIZE = 17; +constexpr size_t HEADER_SIZE = 15; +constexpr size_t CODE_SIZE = 17; void GoboxProtocol::dump_timings_(const RawTimings &timings) const { ESP_LOGD(TAG, "Gobox: size=%u", timings.size()); @@ -39,7 +39,7 @@ void GoboxProtocol::encode(RemoteTransmitData *dst, const GoboxData &data) { } optional GoboxProtocol::decode(RemoteReceiveData src) { - if (src.size() < ((HEADER_SIZE + CODE_SIZE) * 2 + 1)) { + if (static_cast(src.size()) < ((HEADER_SIZE + CODE_SIZE) * 2 + 1)) { return {}; } From e340397b4152e8bbb837095511ff9558c109a14f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 12:10:41 -0500 Subject: [PATCH 90/99] [mipi_spi] Fix clang-tidy sign comparison errors (#11070) --- esphome/components/mipi_spi/mipi_spi.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 00b861f71b..248d5b7104 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -340,7 +340,7 @@ class MipiSpi : public display::Display, this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8); } } else { - for (size_t y = 0; y != h; y++) { + for (size_t y = 0; y != static_cast(h); y++) { if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) { this->write_array(ptr, w); } else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) { @@ -372,8 +372,8 @@ class MipiSpi : public display::Display, uint8_t dbuffer[DISPLAYPIXEL * 48]; uint8_t *dptr = dbuffer; auto stride = x_offset + w + x_pad; // stride in pixels - for (size_t y = 0; y != h; y++) { - for (size_t x = 0; x != w; x++) { + for (size_t y = 0; y != static_cast(h); y++) { + for (size_t x = 0; x != static_cast(w); x++) { auto color_val = ptr[y * stride + x]; if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_16) { // 16 to 18 bit conversion From f4df17673b063e12bd57f5d4d64529625980a242 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 12:15:06 -0500 Subject: [PATCH 91/99] [esp32_ble_server] Refactor property setters to reduce code duplication (#11071) --- .../esp32_ble_server/ble_characteristic.cpp | 44 +++++-------------- .../esp32_ble_server/ble_characteristic.h | 2 + 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index c632165fb7..e947474593 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -147,47 +147,27 @@ bool BLECharacteristic::is_failed() { return this->state_ == FAILED; } -void BLECharacteristic::set_broadcast_property(bool value) { +void BLECharacteristic::set_property_bit_(esp_gatt_char_prop_t bit, bool value) { if (value) { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_BROADCAST); + this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | bit); } else { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_BROADCAST); + this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~bit); } } + +void BLECharacteristic::set_broadcast_property(bool value) { + this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_BROADCAST, value); +} void BLECharacteristic::set_indicate_property(bool value) { - if (value) { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_INDICATE); - } else { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_INDICATE); - } + this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_INDICATE, value); } void BLECharacteristic::set_notify_property(bool value) { - if (value) { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_NOTIFY); - } else { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_NOTIFY); - } -} -void BLECharacteristic::set_read_property(bool value) { - if (value) { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_READ); - } else { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_READ); - } -} -void BLECharacteristic::set_write_property(bool value) { - if (value) { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE); - } else { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE); - } + this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_NOTIFY, value); } +void BLECharacteristic::set_read_property(bool value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_READ, value); } +void BLECharacteristic::set_write_property(bool value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_WRITE, value); } void BLECharacteristic::set_write_no_response_property(bool value) { - if (value) { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE_NR); - } else { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE_NR); - } + this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_WRITE_NR, value); } void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index 4a29683f41..7cceec0ef1 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -97,6 +97,8 @@ class BLECharacteristic { void remove_client_from_notify_list_(uint16_t conn_id); ClientNotificationEntry *find_client_in_notify_list_(uint16_t conn_id); + void set_property_bit_(esp_gatt_char_prop_t bit, bool value); + std::unique_ptr, uint16_t)>> on_write_callback_; std::unique_ptr> on_read_callback_; From 42d1269aaf976d3f7d70e7e55e941163cc5048b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 12:16:25 -0500 Subject: [PATCH 92/99] [esp32_ble_server] Use early returns in is_created() and is_failed() methods (#11072) --- .../esp32_ble_server/ble_characteristic.cpp | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index e947474593..87f562a250 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -125,26 +125,26 @@ bool BLECharacteristic::is_created() { if (this->state_ != CREATING_DEPENDENTS) return false; - bool created = true; for (auto *descriptor : this->descriptors_) { - created &= descriptor->is_created(); + if (!descriptor->is_created()) + return false; } - if (created) - this->state_ = CREATED; - return this->state_ == CREATED; + // All descriptors are created if we reach here + this->state_ = CREATED; + return true; } bool BLECharacteristic::is_failed() { if (this->state_ == FAILED) return true; - bool failed = false; for (auto *descriptor : this->descriptors_) { - failed |= descriptor->is_failed(); + if (descriptor->is_failed()) { + this->state_ = FAILED; + return true; + } } - if (failed) - this->state_ = FAILED; - return this->state_ == FAILED; + return false; } void BLECharacteristic::set_property_bit_(esp_gatt_char_prop_t bit, bool value) { From cba85c0925f56aa93a7416e0e68c74ef82a07c57 Mon Sep 17 00:00:00 2001 From: Stephen Kent Date: Mon, 6 Oct 2025 10:24:58 -0700 Subject: [PATCH 93/99] [remote_receiver] Add signal demodulation support on ESP32 (#8711) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .../components/remote_receiver/__init__.py | 12 ++++++++++++ .../remote_receiver/remote_receiver.h | 4 ++++ .../remote_receiver/remote_receiver_esp32.cpp | 19 ++++++++++++++++++- .../remote_receiver/test.esp32-idf.yaml | 2 ++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 956f240b14..cd2b440645 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -5,6 +5,8 @@ from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, + CONF_CARRIER_DUTY_PERCENT, + CONF_CARRIER_FREQUENCY, CONF_CLOCK_RESOLUTION, CONF_DUMP, CONF_FILTER, @@ -149,6 +151,14 @@ CONFIG_SCHEMA = remote_base.validate_triggers( ), cv.boolean, ), + cv.SplitDefault(CONF_CARRIER_DUTY_PERCENT, esp32=100): cv.All( + cv.only_on_esp32, + cv.percentage_int, + cv.Range(min=1, max=100), + ), + cv.SplitDefault(CONF_CARRIER_FREQUENCY, esp32="0Hz"): cv.All( + cv.only_on_esp32, cv.frequency, cv.int_ + ), } ) .extend(cv.COMPONENT_SCHEMA) @@ -168,6 +178,8 @@ async def to_code(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])) + cg.add(var.set_carrier_duty_percent(config[CONF_CARRIER_DUTY_PERCENT])) + cg.add(var.set_carrier_frequency(config[CONF_CARRIER_FREQUENCY])) 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 45e06e664a..3ddcf353c7 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -64,6 +64,8 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, 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; } + void set_carrier_duty_percent(uint8_t carrier_duty_percent) { this->carrier_duty_percent_ = carrier_duty_percent; } + void set_carrier_frequency(uint32_t carrier_frequency) { this->carrier_frequency_ = carrier_frequency; } #endif void set_buffer_size(uint32_t buffer_size) { this->buffer_size_ = buffer_size; } void set_filter_us(uint32_t filter_us) { this->filter_us_ = filter_us; } @@ -76,6 +78,8 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, uint32_t filter_symbols_{0}; uint32_t receive_symbols_{0}; bool with_dma_{false}; + uint32_t carrier_frequency_{0}; + uint8_t carrier_duty_percent_{100}; esp_err_t error_code_{ESP_OK}; std::string error_string_{""}; #endif diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index 7e1bd3c457..49358eef3f 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -72,6 +72,21 @@ void RemoteReceiverComponent::setup() { return; } + if (this->carrier_frequency_ > 0 && 0 < this->carrier_duty_percent_ && this->carrier_duty_percent_ < 100) { + rmt_carrier_config_t carrier; + memset(&carrier, 0, sizeof(carrier)); + carrier.frequency_hz = this->carrier_frequency_; + carrier.duty_cycle = (float) this->carrier_duty_percent_ / 100.0f; + carrier.flags.polarity_active_low = this->pin_->is_inverted(); + error = rmt_apply_carrier(this->channel_, &carrier); + if (error != ESP_OK) { + this->error_code_ = error; + this->error_string_ = "in rmt_apply_carrier"; + this->mark_failed(); + return; + } + } + rmt_rx_event_callbacks_t callbacks; memset(&callbacks, 0, sizeof(callbacks)); callbacks.on_recv_done = rmt_callback; @@ -111,11 +126,13 @@ void RemoteReceiverComponent::dump_config() { " Filter symbols: %" PRIu32 "\n" " Receive symbols: %" PRIu32 "\n" " Tolerance: %" PRIu32 "%s\n" + " Carrier frequency: %" PRIu32 " hz\n" + " Carrier duty: %u%%\n" " Filter out pulses shorter than: %" PRIu32 " us\n" " Signal is done after %" PRIu32 " us of no changes", 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_); + this->carrier_frequency_, this->carrier_duty_percent_, this->filter_us_, this->idle_us_); 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()); diff --git a/tests/components/remote_receiver/test.esp32-idf.yaml b/tests/components/remote_receiver/test.esp32-idf.yaml index 10dd767598..cdeeab2c4a 100644 --- a/tests/components/remote_receiver/test.esp32-idf.yaml +++ b/tests/components/remote_receiver/test.esp32-idf.yaml @@ -1,6 +1,8 @@ substitutions: pin: GPIO2 clock_resolution: "2000000" + carrier_duty_percent: "25" + carrier_frequency: "30000" filter_symbols: "2" receive_symbols: "4" rmt_symbols: "64" From 75867842eab4c7e6cfa5031fa0d840a6f149fb03 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 12:26:59 -0500 Subject: [PATCH 94/99] [rtttl] Fix clang-tidy sign comparison error (#11065) --- esphome/components/rtttl/rtttl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 2c48105490..b79f27e2e5 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -215,7 +215,7 @@ void Rtttl::loop() { sample[x].right = 0; } - if (x >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) { + if (static_cast(x) >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) { break; } this->samples_sent_++; From 2fa49be17d58b7d0e37cb74585dc09f1c977c01d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 12:27:15 -0500 Subject: [PATCH 95/99] [haier] Fix clang-tidy sign comparison error (#11053) --- esphome/components/haier/hon_climate.cpp | 4 ++-- esphome/components/haier/hon_climate.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 9614bb1e47..76558f2ebb 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -213,7 +213,7 @@ haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameTy this->real_control_packet_size_); this->status_message_callback_.call((const char *) data, data_size); } else { - ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, this->real_control_packet_size_); + ESP_LOGW(TAG, "Status packet too small: %zu (should be >= %zu)", data_size, this->real_control_packet_size_); } switch (this->protocol_phase_) { case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: @@ -827,7 +827,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * size_t expected_size = 2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_; if (size < expected_size) { - ESP_LOGW(TAG, "Unexpected message size %d (expexted >= %d)", size, expected_size); + ESP_LOGW(TAG, "Unexpected message size %u (expexted >= %zu)", size, expected_size); return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; } uint16_t subtype = (((uint16_t) packet_buffer[0]) << 8) + packet_buffer[1]; diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index 58173f8154..a567ab1d89 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -178,7 +178,7 @@ class HonClimate : public HaierClimateBase { int extra_control_packet_bytes_{0}; int extra_sensors_packet_bytes_{4}; int status_message_header_size_{0}; - int real_control_packet_size_{sizeof(hon_protocol::HaierPacketControl)}; + size_t real_control_packet_size_{sizeof(hon_protocol::HaierPacketControl)}; int real_sensors_packet_size_{sizeof(hon_protocol::HaierPacketSensors) + 4}; HonControlMethod control_method_; std::queue control_messages_queue_; From 3d82301c3d40bfd3a67a7fe725cde0368e6e7a57 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 12:28:43 -0500 Subject: [PATCH 96/99] [graph] Fix clang-tidy sign comparison error (#11051) --- esphome/components/graph/graph.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index 5abf2ade0d..ac6ace96ee 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -179,7 +179,7 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo if (b) { int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset; auto draw_pixel_at = [&buff, c, y_offset, this](int16_t x, int16_t y) { - if (y >= y_offset && y < y_offset + this->height_) + if (y >= y_offset && static_cast(y) < y_offset + this->height_) buff->draw_pixel_at(x, y, c); }; if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) { From fdd422c42a78f0d611c602f7cfbea53cb5d660cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 13:14:52 -0500 Subject: [PATCH 97/99] [tormatic] Fix clang-tidy sign comparison error (#11075) --- esphome/components/tormatic/tormatic_cover.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/tormatic/tormatic_cover.cpp b/esphome/components/tormatic/tormatic_cover.cpp index be412d62a8..ef93964a28 100644 --- a/esphome/components/tormatic/tormatic_cover.cpp +++ b/esphome/components/tormatic/tormatic_cover.cpp @@ -251,7 +251,7 @@ void Tormatic::stop_at_target_() { // Read a GateStatus from the unit. The unit only sends messages in response to // status requests or commands, so a message needs to be sent first. optional Tormatic::read_gate_status_() { - if (this->available() < sizeof(MessageHeader)) { + if (this->available() < static_cast(sizeof(MessageHeader))) { return {}; } From ad296a7d744a8ef8292e33094684db55c030b16e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 13:20:56 -0500 Subject: [PATCH 98/99] [uponor_smatrix] Fix clang-tidy sign comparison errors (#11076) --- .../uponor_smatrix/climate/uponor_smatrix_climate.cpp | 2 +- .../uponor_smatrix/sensor/uponor_smatrix_sensor.cpp | 2 +- esphome/components/uponor_smatrix/uponor_smatrix.cpp | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp index d7e672d8cf..19a9112c73 100644 --- a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp +++ b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp @@ -58,7 +58,7 @@ void UponorSmatrixClimate::control(const climate::ClimateCall &call) { } void UponorSmatrixClimate::on_device_data(const UponorSmatrixData *data, size_t data_len) { - for (int i = 0; i < data_len; i++) { + for (size_t i = 0; i < data_len; i++) { switch (data[i].id) { case UPONOR_ID_TARGET_TEMP_MIN: this->min_temperature_ = raw_to_celsius(data[i].value); diff --git a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp index 452660dc14..a1d0db214f 100644 --- a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp +++ b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp @@ -18,7 +18,7 @@ void UponorSmatrixSensor::dump_config() { } void UponorSmatrixSensor::on_device_data(const UponorSmatrixData *data, size_t data_len) { - for (int i = 0; i < data_len; i++) { + for (size_t i = 0; i < data_len; i++) { switch (data[i].id) { case UPONOR_ID_ROOM_TEMP: if (this->temperature_sensor_ != nullptr) diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.cpp b/esphome/components/uponor_smatrix/uponor_smatrix.cpp index a0017518bf..867305059f 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.cpp +++ b/esphome/components/uponor_smatrix/uponor_smatrix.cpp @@ -122,7 +122,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { // Decode packet payload data for easy access UponorSmatrixData data[data_len]; - for (int i = 0; i < data_len; i++) { + for (size_t i = 0; i < data_len; i++) { data[i].id = packet[(i * 3) + 4]; data[i].value = encode_uint16(packet[(i * 3) + 5], packet[(i * 3) + 6]); } @@ -135,7 +135,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { // thermostat sending both room temperature and time information. bool found_temperature = false; bool found_time = false; - for (int i = 0; i < data_len; i++) { + for (size_t i = 0; i < data_len; i++) { if (data[i].id == UPONOR_ID_ROOM_TEMP) found_temperature = true; if (data[i].id == UPONOR_ID_DATETIME1) @@ -181,7 +181,7 @@ bool UponorSmatrixComponent::send(uint16_t device_address, const UponorSmatrixDa packet.push_back(device_address >> 8); packet.push_back(device_address >> 0); - for (int i = 0; i < data_len; i++) { + for (size_t i = 0; i < data_len; i++) { packet.push_back(data[i].id); packet.push_back(data[i].value >> 8); packet.push_back(data[i].value >> 0); From 9adc3bd94374cba5105525623ce48c5e7464d0bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 13:24:05 -0500 Subject: [PATCH 99/99] [veml7700] Fix clang-tidy sign comparison errors (#11078) --- esphome/components/veml7700/veml7700.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/esphome/components/veml7700/veml7700.cpp b/esphome/components/veml7700/veml7700.cpp index c3b601e288..eb286ba21b 100644 --- a/esphome/components/veml7700/veml7700.cpp +++ b/esphome/components/veml7700/veml7700.cpp @@ -1,6 +1,7 @@ #include "veml7700.h" #include "esphome/core/application.h" #include "esphome/core/log.h" +#include namespace esphome { namespace veml7700 { @@ -12,30 +13,30 @@ static float reduce_to_zero(float a, float b) { return (a > b) ? (a - b) : 0; } template T get_next(const T (&array)[size], const T val) { size_t i = 0; - size_t idx = -1; - while (idx == -1 && i < size) { + size_t idx = std::numeric_limits::max(); + while (idx == std::numeric_limits::max() && i < size) { if (array[i] == val) { idx = i; break; } i++; } - if (idx == -1 || i + 1 >= size) + if (idx == std::numeric_limits::max() || i + 1 >= size) return val; return array[i + 1]; } template T get_prev(const T (&array)[size], const T val) { size_t i = size - 1; - size_t idx = -1; - while (idx == -1 && i > 0) { + size_t idx = std::numeric_limits::max(); + while (idx == std::numeric_limits::max() && i > 0) { if (array[i] == val) { idx = i; break; } i--; } - if (idx == -1 || i == 0) + if (idx == std::numeric_limits::max() || i == 0) return val; return array[i - 1]; }