From fd3c05b42e8097487fa22daf797e9992d35e7160 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Wed, 1 Oct 2025 03:33:56 +0200 Subject: [PATCH 01/14] [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/14] [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/14] [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/14] [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/14] [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/14] [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/14] 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/14] [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/14] [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/14] [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/14] [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/14] [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/14] [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 f4aea8fa7acb76cbba38a0c1305708e78a51e246 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Oct 2025 16:35:26 +0200 Subject: [PATCH 14/14] tweak --- esphome/components/api/api_connection.cpp | 15 ++++++++------- esphome/components/api/api_connection.h | 4 ++-- esphome/components/api/api_server.cpp | 3 ++- .../voice_assistant/voice_assistant.cpp | 5 +++-- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 44b1bf8723..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 (%s) is unresponsive; disconnecting", this->get_name(), this->get_peername()); + 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 (%s) disconnected", this->get_name(), this->get_peername()); + 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 (%s) connected", this->get_name(), this->get_peername()); + 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 (%s) no authentication", this->get_name(), this->get_peername()); + 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 (%s) no connection setup", this->get_name(), this->get_peername()); + 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 %s errno=%d", this->get_name(), this->get_peername(), 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 54535eaccd..a21574f6d5 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -270,8 +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; - const char *get_name() const { return this->client_info_.name.c_str(); } - const char *get_peername() const { return this->client_info_.peername.c_str(); } + 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_server.cpp b/esphome/components/api/api_server.cpp index 2b41a71769..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 (%s): Network down; disconnect", client->get_name(), client->get_peername()); + 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 bb429ca7df..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 (%s)", this->api_client_->get_name(), this->api_client_->get_peername()); - ESP_LOGE(TAG, "New client: %s (%s)", client->get_name(), client->get_peername()); + 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; }