From fb35ddebb9915f6bda82e4050943ee5de1ba0360 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 08:22:07 -0600 Subject: [PATCH 01/43] [display] Make COLOR_OFF and COLOR_ON inline constexpr (#14044) --- esphome/components/display/display.cpp | 3 +-- esphome/components/display/display.h | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index ebc3c0a9f6..53a087803c 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -9,8 +9,7 @@ namespace esphome { namespace display { static const char *const TAG = "display"; -const Color COLOR_OFF(0, 0, 0, 0); -const Color COLOR_ON(255, 255, 255, 255); +// COLOR_OFF and COLOR_ON are now inline constexpr in display.h void Display::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } void Display::clear() { this->fill(COLOR_OFF); } diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 47d40915aa..e40f6ec963 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -298,9 +298,9 @@ using display_writer_t = DisplayWriter; } /// Turn the pixel OFF. -extern const Color COLOR_OFF; +inline constexpr Color COLOR_OFF(0, 0, 0, 0); /// Turn the pixel ON. -extern const Color COLOR_ON; +inline constexpr Color COLOR_ON(255, 255, 255, 255); class BaseImage { public: From fb89900c64f2a88d6f86feaa43e453af8d83b53c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 08:22:36 -0600 Subject: [PATCH 02/43] [core] Make setup_priority and component state constants constexpr (#14041) --- esphome/core/component.cpp | 32 ++--------------------- esphome/core/component.h | 52 +++++++++++++++++++------------------- 2 files changed, 28 insertions(+), 56 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 90aa36f4db..a452f4d400 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -76,36 +76,8 @@ void store_component_error_message(const Component *component, const char *messa } } // namespace -namespace setup_priority { - -const float BUS = 1000.0f; -const float IO = 900.0f; -const float HARDWARE = 800.0f; -const float DATA = 600.0f; -const float PROCESSOR = 400.0; -const float BLUETOOTH = 350.0f; -const float AFTER_BLUETOOTH = 300.0f; -const float WIFI = 250.0f; -const float ETHERNET = 250.0f; -const float BEFORE_CONNECTION = 220.0f; -const float AFTER_WIFI = 200.0f; -const float AFTER_CONNECTION = 100.0f; -const float LATE = -100.0f; - -} // namespace setup_priority - -// Component state uses bits 0-2 (8 states, 5 used) -const uint8_t COMPONENT_STATE_MASK = 0x07; -const uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00; -const uint8_t COMPONENT_STATE_SETUP = 0x01; -const uint8_t COMPONENT_STATE_LOOP = 0x02; -const uint8_t COMPONENT_STATE_FAILED = 0x03; -const uint8_t COMPONENT_STATE_LOOP_DONE = 0x04; -// Status LED uses bits 3-4 -const uint8_t STATUS_LED_MASK = 0x18; -const uint8_t STATUS_LED_OK = 0x00; -const uint8_t STATUS_LED_WARNING = 0x08; // Bit 3 -const uint8_t STATUS_LED_ERROR = 0x10; // Bit 4 +// setup_priority, component state, and status LED constants are now +// constexpr in component.h const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again diff --git a/esphome/core/component.h b/esphome/core/component.h index 9ab77cc2f9..848bc0ba35 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -21,33 +21,31 @@ struct LogString; namespace setup_priority { /// For communication buses like i2c/spi -extern const float BUS; +inline constexpr float BUS = 1000.0f; /// For components that represent GPIO pins like PCF8573 -extern const float IO; +inline constexpr float IO = 900.0f; /// For components that deal with hardware and are very important like GPIO switch -extern const float HARDWARE; +inline constexpr float HARDWARE = 800.0f; /// For components that import data from directly connected sensors like DHT. -extern const float DATA; -/// Alias for DATA (here for compatibility reasons) -extern const float HARDWARE_LATE; +inline constexpr float DATA = 600.0f; /// For components that use data from sensors like displays -extern const float PROCESSOR; -extern const float BLUETOOTH; -extern const float AFTER_BLUETOOTH; -extern const float WIFI; -extern const float ETHERNET; +inline constexpr float PROCESSOR = 400.0f; +inline constexpr float BLUETOOTH = 350.0f; +inline constexpr float AFTER_BLUETOOTH = 300.0f; +inline constexpr float WIFI = 250.0f; +inline constexpr float ETHERNET = 250.0f; /// For components that should be initialized after WiFi and before API is connected. -extern const float BEFORE_CONNECTION; +inline constexpr float BEFORE_CONNECTION = 220.0f; /// For components that should be initialized after WiFi is connected. -extern const float AFTER_WIFI; +inline constexpr float AFTER_WIFI = 200.0f; /// For components that should be initialized after a data connection (API/MQTT) is connected. -extern const float AFTER_CONNECTION; +inline constexpr float AFTER_CONNECTION = 100.0f; /// For components that should be initialized at the very end of the setup process. -extern const float LATE; +inline constexpr float LATE = -100.0f; } // namespace setup_priority -static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; +inline constexpr uint32_t SCHEDULER_DONT_RUN = 4294967295UL; /// Type-safe scheduler IDs for core base classes. /// Uses a separate NameType (NUMERIC_ID_INTERNAL) so IDs can never collide @@ -65,16 +63,18 @@ void log_update_interval(const char *tag, PollingComponent *component); #define LOG_UPDATE_INTERVAL(this) log_update_interval(TAG, this) -extern const uint8_t COMPONENT_STATE_MASK; -extern const uint8_t COMPONENT_STATE_CONSTRUCTION; -extern const uint8_t COMPONENT_STATE_SETUP; -extern const uint8_t COMPONENT_STATE_LOOP; -extern const uint8_t COMPONENT_STATE_FAILED; -extern const uint8_t COMPONENT_STATE_LOOP_DONE; -extern const uint8_t STATUS_LED_MASK; -extern const uint8_t STATUS_LED_OK; -extern const uint8_t STATUS_LED_WARNING; -extern const uint8_t STATUS_LED_ERROR; +// Component state uses bits 0-2 (8 states, 5 used) +inline constexpr uint8_t COMPONENT_STATE_MASK = 0x07; +inline constexpr uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00; +inline constexpr uint8_t COMPONENT_STATE_SETUP = 0x01; +inline constexpr uint8_t COMPONENT_STATE_LOOP = 0x02; +inline constexpr uint8_t COMPONENT_STATE_FAILED = 0x03; +inline constexpr uint8_t COMPONENT_STATE_LOOP_DONE = 0x04; +// Status LED uses bits 3-4 +inline constexpr uint8_t STATUS_LED_MASK = 0x18; +inline constexpr uint8_t STATUS_LED_OK = 0x00; +inline constexpr uint8_t STATUS_LED_WARNING = 0x08; +inline constexpr uint8_t STATUS_LED_ERROR = 0x10; // Remove before 2026.8.0 enum class RetryResult { DONE, RETRY }; From 652c669777ecaa125f575aa7eac72bf7f5cc5a1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:08:02 -0600 Subject: [PATCH 03/43] Bump pillow from 11.3.0 to 12.1.1 (#14048) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a0a29ad30a..77d30ecd3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import esphome-glyphsets==0.2.0 -pillow==11.3.0 +pillow==12.1.1 resvg-py==0.2.6 freetype-py==2.5.1 jinja2==3.1.6 From f73bcc0e7bebba965a76859f680ac16681833a4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:08:12 -0600 Subject: [PATCH 04/43] Bump cryptography from 45.0.1 to 46.0.5 (#14049) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 77d30ecd3d..2e6268284a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -cryptography==45.0.1 +cryptography==46.0.5 voluptuous==0.16.0 PyYAML==6.0.3 paho-mqtt==1.6.1 From 9cd7b0b32b1a4c27a3b0673b1d10187e69e55109 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:09:33 -0500 Subject: [PATCH 05/43] [external_components] Clean up incomplete clone on failed ref fetch (#14051) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 4 +- esphome/git.py | 47 ++++++----- esphome/helpers.py | 20 ++++- esphome/writer.py | 27 +------ tests/unit_tests/test_git.py | 113 ++++++++++++++++++++++++++- 5 files changed, 162 insertions(+), 49 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b78b945a24..8b3e1afea6 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -44,9 +44,9 @@ from esphome.const import ( from esphome.core import CORE, HexInt, TimePeriod from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv -from esphome.helpers import copy_file_if_changed, write_file_if_changed +from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed from esphome.types import ConfigType -from esphome.writer import clean_cmake_cache, rmtree +from esphome.writer import clean_cmake_cache from .boards import BOARDS, STANDARD_BOARDS from .const import ( # noqa diff --git a/esphome/git.py b/esphome/git.py index 4ff07ffe75..a45768b5cd 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -5,12 +5,12 @@ import hashlib import logging from pathlib import Path import re -import shutil import subprocess import urllib.parse import esphome.config_validation as cv from esphome.core import CORE, TimePeriodSeconds +from esphome.helpers import rmtree _LOGGER = logging.getLogger(__name__) @@ -115,24 +115,35 @@ def clone_or_update( if not repo_dir.is_dir(): _LOGGER.info("Cloning %s", key) _LOGGER.debug("Location: %s", repo_dir) - cmd = ["git", "clone", "--depth=1"] - cmd += ["--", url, str(repo_dir)] - run_git_command(cmd) + try: + cmd = ["git", "clone", "--depth=1"] + cmd += ["--", url, str(repo_dir)] + run_git_command(cmd) - if ref is not None: - # We need to fetch the PR branch first, otherwise git will complain - # about missing objects - _LOGGER.info("Fetching %s", ref) - run_git_command(["git", "fetch", "--", "origin", ref], git_dir=repo_dir) - run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], git_dir=repo_dir) + if ref is not None: + # We need to fetch the PR branch first, otherwise git will complain + # about missing objects + _LOGGER.info("Fetching %s", ref) + run_git_command(["git", "fetch", "--", "origin", ref], git_dir=repo_dir) + run_git_command( + ["git", "reset", "--hard", "FETCH_HEAD"], git_dir=repo_dir + ) - if submodules is not None: - _LOGGER.info( - "Initializing submodules (%s) for %s", ", ".join(submodules), key - ) - run_git_command( - ["git", "submodule", "update", "--init"] + submodules, git_dir=repo_dir - ) + if submodules is not None: + _LOGGER.info( + "Initializing submodules (%s) for %s", ", ".join(submodules), key + ) + run_git_command( + ["git", "submodule", "update", "--init"] + submodules, + git_dir=repo_dir, + ) + except GitException: + # Remove incomplete clone to prevent stale state. Without this, + # a failed ref fetch leaves a clone on the default branch, and + # subsequent calls skip the update due to the refresh window. + if repo_dir.is_dir(): + rmtree(repo_dir) + raise else: # Check refresh needed @@ -193,7 +204,7 @@ def clone_or_update( err, ) _LOGGER.info("Removing broken repository at %s", repo_dir) - shutil.rmtree(repo_dir) + rmtree(repo_dir) _LOGGER.info("Successfully removed broken repository, re-cloning...") # Recursively call clone_or_update to re-clone diff --git a/esphome/helpers.py b/esphome/helpers.py index ae142b7f8b..145ebd4096 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -8,6 +8,7 @@ from pathlib import Path import platform import re import shutil +import stat import tempfile from typing import TYPE_CHECKING from urllib.parse import urlparse @@ -354,6 +355,23 @@ def is_ha_addon(): return get_bool_env("ESPHOME_IS_HA_ADDON") +def rmtree(path: Path | str) -> None: + """Remove a directory tree, handling read-only files on Windows. + + On Windows, git pack files and other files may be marked read-only, + causing shutil.rmtree to fail. This handles that by removing the + read-only flag and retrying. + """ + + def _onerror(func, path, exc_info): + if os.access(path, os.W_OK): + raise exc_info[1].with_traceback(exc_info[2]) + os.chmod(path, stat.S_IWUSR | stat.S_IRUSR) + func(path) + + shutil.rmtree(path, onerror=_onerror) + + def walk_files(path: Path): for root, _, files in os.walk(path): for name in files: @@ -481,8 +499,6 @@ def list_starts_with(list_, sub): def file_compare(path1: Path, path2: Path) -> bool: """Return True if the files path1 and path2 have the same contents.""" - import stat - try: stat1, stat2 = path1.stat(), path2.stat() except OSError: diff --git a/esphome/writer.py b/esphome/writer.py index 661118e518..fd4c811fb3 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -1,14 +1,10 @@ -from collections.abc import Callable import importlib import json import logging import os from pathlib import Path import re -import shutil -import stat import time -from types import TracebackType from esphome import loader from esphome.config import iter_component_configs, iter_components @@ -25,6 +21,7 @@ from esphome.helpers import ( get_str_env, is_ha_addon, read_file, + rmtree, walk_files, write_file, write_file_if_changed, @@ -404,28 +401,6 @@ def clean_cmake_cache(): pioenvs_cmake_path.unlink() -def _rmtree_error_handler( - func: Callable[[str], object], - path: str, - exc_info: tuple[type[BaseException], BaseException, TracebackType | None], -) -> None: - """Error handler for shutil.rmtree to handle read-only files on Windows. - - On Windows, git pack files and other files may be marked read-only, - causing shutil.rmtree to fail with "Access is denied". This handler - removes the read-only flag and retries the deletion. - """ - if os.access(path, os.W_OK): - raise exc_info[1].with_traceback(exc_info[2]) - os.chmod(path, stat.S_IWUSR | stat.S_IRUSR) - func(path) - - -def rmtree(path: Path | str) -> None: - """Remove a directory tree, handling read-only files on Windows.""" - shutil.rmtree(path, onerror=_rmtree_error_handler) - - def clean_build(clear_pio_cache: bool = True): # Allow skipping cache cleaning for integration tests if os.environ.get("ESPHOME_SKIP_CLEAN_BUILD"): diff --git a/tests/unit_tests/test_git.py b/tests/unit_tests/test_git.py index 0411fe5e43..745dfad487 100644 --- a/tests/unit_tests/test_git.py +++ b/tests/unit_tests/test_git.py @@ -656,7 +656,7 @@ def test_clone_or_update_recover_broken_flag_prevents_infinite_loop( # Should raise on the second attempt when _recover_broken=False # This hits the "if not _recover_broken: raise" path with ( - unittest.mock.patch("esphome.git.shutil.rmtree", side_effect=mock_rmtree), + unittest.mock.patch("esphome.git.rmtree", side_effect=mock_rmtree), pytest.raises(GitCommandError, match="fatal: unable to write new index file"), ): git.clone_or_update( @@ -671,3 +671,114 @@ def test_clone_or_update_recover_broken_flag_prevents_infinite_loop( stash_calls = [c for c in call_list if "stash" in c[0][0]] # Should have exactly two stash calls assert len(stash_calls) == 2 + + +def test_clone_or_update_cleans_up_on_failed_ref_fetch( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that a failed ref fetch removes the incomplete clone directory. + + When cloning with a specific ref, if `git clone` succeeds but the + subsequent `git fetch ` fails, the clone directory should be + removed so the next attempt starts fresh instead of finding a stale + clone on the default branch. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "pull/123/head" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type == "clone": + # Simulate successful clone by creating the directory + repo_dir.mkdir(parents=True, exist_ok=True) + (repo_dir / ".git").mkdir(exist_ok=True) + return "" + if cmd_type == "fetch": + raise GitCommandError("fatal: couldn't find remote ref pull/123/head") + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + refresh = TimePeriodSeconds(days=1) + + with pytest.raises(GitCommandError, match="couldn't find remote ref"): + git.clone_or_update( + url=url, + ref=ref, + refresh=refresh, + domain=domain, + ) + + # The incomplete clone directory should have been removed + assert not repo_dir.exists() + + # Verify clone was attempted then fetch failed + call_list = mock_run_git_command.call_args_list + clone_calls = [c for c in call_list if "clone" in c[0][0]] + assert len(clone_calls) == 1 + fetch_calls = [c for c in call_list if "fetch" in c[0][0]] + assert len(fetch_calls) == 1 + + +def test_clone_or_update_stale_clone_is_retried_after_cleanup( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that after cleanup, a subsequent call does a fresh clone. + + This is the full scenario: first call fails at fetch (directory cleaned up), + second call sees no directory and clones fresh. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "pull/123/head" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + call_count = {"clone": 0, "fetch": 0} + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type == "clone": + call_count["clone"] += 1 + repo_dir.mkdir(parents=True, exist_ok=True) + (repo_dir / ".git").mkdir(exist_ok=True) + return "" + if cmd_type == "fetch": + call_count["fetch"] += 1 + if call_count["fetch"] == 1: + # First fetch fails + raise GitCommandError("fatal: couldn't find remote ref pull/123/head") + # Second fetch succeeds + return "" + if cmd_type == "reset": + return "" + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + refresh = TimePeriodSeconds(days=1) + + # First call: clone succeeds, fetch fails, directory cleaned up + with pytest.raises(GitCommandError, match="couldn't find remote ref"): + git.clone_or_update(url=url, ref=ref, refresh=refresh, domain=domain) + + assert not repo_dir.exists() + + # Second call: fresh clone + fetch succeeds + result_dir, _ = git.clone_or_update( + url=url, ref=ref, refresh=refresh, domain=domain + ) + + assert result_dir == repo_dir + assert repo_dir.exists() + assert call_count["clone"] == 2 + assert call_count["fetch"] == 2 From 6b8264fcaa56e7d9868733f9b3e39135c886f67f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:09:33 -0500 Subject: [PATCH 06/43] [external_components] Clean up incomplete clone on failed ref fetch (#14051) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 4 +- esphome/git.py | 47 ++++++----- esphome/helpers.py | 20 ++++- esphome/writer.py | 27 +------ tests/unit_tests/test_git.py | 113 ++++++++++++++++++++++++++- 5 files changed, 162 insertions(+), 49 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b78b945a24..8b3e1afea6 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -44,9 +44,9 @@ from esphome.const import ( from esphome.core import CORE, HexInt, TimePeriod from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv -from esphome.helpers import copy_file_if_changed, write_file_if_changed +from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed from esphome.types import ConfigType -from esphome.writer import clean_cmake_cache, rmtree +from esphome.writer import clean_cmake_cache from .boards import BOARDS, STANDARD_BOARDS from .const import ( # noqa diff --git a/esphome/git.py b/esphome/git.py index 4ff07ffe75..a45768b5cd 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -5,12 +5,12 @@ import hashlib import logging from pathlib import Path import re -import shutil import subprocess import urllib.parse import esphome.config_validation as cv from esphome.core import CORE, TimePeriodSeconds +from esphome.helpers import rmtree _LOGGER = logging.getLogger(__name__) @@ -115,24 +115,35 @@ def clone_or_update( if not repo_dir.is_dir(): _LOGGER.info("Cloning %s", key) _LOGGER.debug("Location: %s", repo_dir) - cmd = ["git", "clone", "--depth=1"] - cmd += ["--", url, str(repo_dir)] - run_git_command(cmd) + try: + cmd = ["git", "clone", "--depth=1"] + cmd += ["--", url, str(repo_dir)] + run_git_command(cmd) - if ref is not None: - # We need to fetch the PR branch first, otherwise git will complain - # about missing objects - _LOGGER.info("Fetching %s", ref) - run_git_command(["git", "fetch", "--", "origin", ref], git_dir=repo_dir) - run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], git_dir=repo_dir) + if ref is not None: + # We need to fetch the PR branch first, otherwise git will complain + # about missing objects + _LOGGER.info("Fetching %s", ref) + run_git_command(["git", "fetch", "--", "origin", ref], git_dir=repo_dir) + run_git_command( + ["git", "reset", "--hard", "FETCH_HEAD"], git_dir=repo_dir + ) - if submodules is not None: - _LOGGER.info( - "Initializing submodules (%s) for %s", ", ".join(submodules), key - ) - run_git_command( - ["git", "submodule", "update", "--init"] + submodules, git_dir=repo_dir - ) + if submodules is not None: + _LOGGER.info( + "Initializing submodules (%s) for %s", ", ".join(submodules), key + ) + run_git_command( + ["git", "submodule", "update", "--init"] + submodules, + git_dir=repo_dir, + ) + except GitException: + # Remove incomplete clone to prevent stale state. Without this, + # a failed ref fetch leaves a clone on the default branch, and + # subsequent calls skip the update due to the refresh window. + if repo_dir.is_dir(): + rmtree(repo_dir) + raise else: # Check refresh needed @@ -193,7 +204,7 @@ def clone_or_update( err, ) _LOGGER.info("Removing broken repository at %s", repo_dir) - shutil.rmtree(repo_dir) + rmtree(repo_dir) _LOGGER.info("Successfully removed broken repository, re-cloning...") # Recursively call clone_or_update to re-clone diff --git a/esphome/helpers.py b/esphome/helpers.py index ae142b7f8b..145ebd4096 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -8,6 +8,7 @@ from pathlib import Path import platform import re import shutil +import stat import tempfile from typing import TYPE_CHECKING from urllib.parse import urlparse @@ -354,6 +355,23 @@ def is_ha_addon(): return get_bool_env("ESPHOME_IS_HA_ADDON") +def rmtree(path: Path | str) -> None: + """Remove a directory tree, handling read-only files on Windows. + + On Windows, git pack files and other files may be marked read-only, + causing shutil.rmtree to fail. This handles that by removing the + read-only flag and retrying. + """ + + def _onerror(func, path, exc_info): + if os.access(path, os.W_OK): + raise exc_info[1].with_traceback(exc_info[2]) + os.chmod(path, stat.S_IWUSR | stat.S_IRUSR) + func(path) + + shutil.rmtree(path, onerror=_onerror) + + def walk_files(path: Path): for root, _, files in os.walk(path): for name in files: @@ -481,8 +499,6 @@ def list_starts_with(list_, sub): def file_compare(path1: Path, path2: Path) -> bool: """Return True if the files path1 and path2 have the same contents.""" - import stat - try: stat1, stat2 = path1.stat(), path2.stat() except OSError: diff --git a/esphome/writer.py b/esphome/writer.py index 661118e518..fd4c811fb3 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -1,14 +1,10 @@ -from collections.abc import Callable import importlib import json import logging import os from pathlib import Path import re -import shutil -import stat import time -from types import TracebackType from esphome import loader from esphome.config import iter_component_configs, iter_components @@ -25,6 +21,7 @@ from esphome.helpers import ( get_str_env, is_ha_addon, read_file, + rmtree, walk_files, write_file, write_file_if_changed, @@ -404,28 +401,6 @@ def clean_cmake_cache(): pioenvs_cmake_path.unlink() -def _rmtree_error_handler( - func: Callable[[str], object], - path: str, - exc_info: tuple[type[BaseException], BaseException, TracebackType | None], -) -> None: - """Error handler for shutil.rmtree to handle read-only files on Windows. - - On Windows, git pack files and other files may be marked read-only, - causing shutil.rmtree to fail with "Access is denied". This handler - removes the read-only flag and retries the deletion. - """ - if os.access(path, os.W_OK): - raise exc_info[1].with_traceback(exc_info[2]) - os.chmod(path, stat.S_IWUSR | stat.S_IRUSR) - func(path) - - -def rmtree(path: Path | str) -> None: - """Remove a directory tree, handling read-only files on Windows.""" - shutil.rmtree(path, onerror=_rmtree_error_handler) - - def clean_build(clear_pio_cache: bool = True): # Allow skipping cache cleaning for integration tests if os.environ.get("ESPHOME_SKIP_CLEAN_BUILD"): diff --git a/tests/unit_tests/test_git.py b/tests/unit_tests/test_git.py index 0411fe5e43..745dfad487 100644 --- a/tests/unit_tests/test_git.py +++ b/tests/unit_tests/test_git.py @@ -656,7 +656,7 @@ def test_clone_or_update_recover_broken_flag_prevents_infinite_loop( # Should raise on the second attempt when _recover_broken=False # This hits the "if not _recover_broken: raise" path with ( - unittest.mock.patch("esphome.git.shutil.rmtree", side_effect=mock_rmtree), + unittest.mock.patch("esphome.git.rmtree", side_effect=mock_rmtree), pytest.raises(GitCommandError, match="fatal: unable to write new index file"), ): git.clone_or_update( @@ -671,3 +671,114 @@ def test_clone_or_update_recover_broken_flag_prevents_infinite_loop( stash_calls = [c for c in call_list if "stash" in c[0][0]] # Should have exactly two stash calls assert len(stash_calls) == 2 + + +def test_clone_or_update_cleans_up_on_failed_ref_fetch( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that a failed ref fetch removes the incomplete clone directory. + + When cloning with a specific ref, if `git clone` succeeds but the + subsequent `git fetch ` fails, the clone directory should be + removed so the next attempt starts fresh instead of finding a stale + clone on the default branch. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "pull/123/head" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type == "clone": + # Simulate successful clone by creating the directory + repo_dir.mkdir(parents=True, exist_ok=True) + (repo_dir / ".git").mkdir(exist_ok=True) + return "" + if cmd_type == "fetch": + raise GitCommandError("fatal: couldn't find remote ref pull/123/head") + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + refresh = TimePeriodSeconds(days=1) + + with pytest.raises(GitCommandError, match="couldn't find remote ref"): + git.clone_or_update( + url=url, + ref=ref, + refresh=refresh, + domain=domain, + ) + + # The incomplete clone directory should have been removed + assert not repo_dir.exists() + + # Verify clone was attempted then fetch failed + call_list = mock_run_git_command.call_args_list + clone_calls = [c for c in call_list if "clone" in c[0][0]] + assert len(clone_calls) == 1 + fetch_calls = [c for c in call_list if "fetch" in c[0][0]] + assert len(fetch_calls) == 1 + + +def test_clone_or_update_stale_clone_is_retried_after_cleanup( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that after cleanup, a subsequent call does a fresh clone. + + This is the full scenario: first call fails at fetch (directory cleaned up), + second call sees no directory and clones fresh. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "pull/123/head" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + call_count = {"clone": 0, "fetch": 0} + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type == "clone": + call_count["clone"] += 1 + repo_dir.mkdir(parents=True, exist_ok=True) + (repo_dir / ".git").mkdir(exist_ok=True) + return "" + if cmd_type == "fetch": + call_count["fetch"] += 1 + if call_count["fetch"] == 1: + # First fetch fails + raise GitCommandError("fatal: couldn't find remote ref pull/123/head") + # Second fetch succeeds + return "" + if cmd_type == "reset": + return "" + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + refresh = TimePeriodSeconds(days=1) + + # First call: clone succeeds, fetch fails, directory cleaned up + with pytest.raises(GitCommandError, match="couldn't find remote ref"): + git.clone_or_update(url=url, ref=ref, refresh=refresh, domain=domain) + + assert not repo_dir.exists() + + # Second call: fresh clone + fetch succeeds + result_dir, _ = git.clone_or_update( + url=url, ref=ref, refresh=refresh, domain=domain + ) + + assert result_dir == repo_dir + assert repo_dir.exists() + assert call_count["clone"] == 2 + assert call_count["fetch"] == 2 From ab572c2882c499f5e517f10c704ced98221543f9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Feb 2026 08:03:44 +1300 Subject: [PATCH 07/43] Bump version to 2026.2.0b5 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index f9deb32899..88565b4a83 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 = 2026.2.0b4 +PROJECT_NUMBER = 2026.2.0b5 # 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 f494b5d41e..b746a55cd4 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.0b4" +__version__ = "2026.2.0b5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 2c89cded4b50130cf3a570971705f488f0e27fed Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:30:04 +1300 Subject: [PATCH 08/43] Bump version to 2026.2.0 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 88565b4a83..38135f9106 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 = 2026.2.0b5 +PROJECT_NUMBER = 2026.2.0 # 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 b746a55cd4..9115055e7b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.0b5" +__version__ = "2026.2.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 4a038978d27b4ed193de8fb934ff32221ae9a31f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 15:04:14 -0600 Subject: [PATCH 09/43] [pca9685] Make mode constants inline constexpr (#14042) --- esphome/components/pca9685/pca9685_output.cpp | 6 +----- esphome/components/pca9685/pca9685_output.h | 10 +++++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/esphome/components/pca9685/pca9685_output.cpp b/esphome/components/pca9685/pca9685_output.cpp index 77e3d5a6c6..89a6bcdcc0 100644 --- a/esphome/components/pca9685/pca9685_output.cpp +++ b/esphome/components/pca9685/pca9685_output.cpp @@ -8,11 +8,7 @@ namespace pca9685 { static const char *const TAG = "pca9685"; -const uint8_t PCA9685_MODE_INVERTED = 0x10; -const uint8_t PCA9685_MODE_OUTPUT_ONACK = 0x08; -const uint8_t PCA9685_MODE_OUTPUT_TOTEM_POLE = 0x04; -const uint8_t PCA9685_MODE_OUTNE_HIGHZ = 0x02; -const uint8_t PCA9685_MODE_OUTNE_LOW = 0x01; +// PCA9685 mode constants are now inline constexpr in pca9685_output.h static const uint8_t PCA9685_REGISTER_SOFTWARE_RESET = 0x06; static const uint8_t PCA9685_REGISTER_MODE1 = 0x00; diff --git a/esphome/components/pca9685/pca9685_output.h b/esphome/components/pca9685/pca9685_output.h index 288c923d4c..785cc974da 100644 --- a/esphome/components/pca9685/pca9685_output.h +++ b/esphome/components/pca9685/pca9685_output.h @@ -13,15 +13,15 @@ enum class PhaseBalancer { }; /// Inverts polarity of channel output signal -extern const uint8_t PCA9685_MODE_INVERTED; +inline constexpr uint8_t PCA9685_MODE_INVERTED = 0x10; /// Channel update happens upon ACK (post-set) rather than on STOP (endTransmission) -extern const uint8_t PCA9685_MODE_OUTPUT_ONACK; +inline constexpr uint8_t PCA9685_MODE_OUTPUT_ONACK = 0x08; /// Use a totem-pole (push-pull) style output rather than an open-drain structure. -extern const uint8_t PCA9685_MODE_OUTPUT_TOTEM_POLE; +inline constexpr uint8_t PCA9685_MODE_OUTPUT_TOTEM_POLE = 0x04; /// For active low output enable, sets channel output to high-impedance state -extern const uint8_t PCA9685_MODE_OUTNE_HIGHZ; +inline constexpr uint8_t PCA9685_MODE_OUTNE_HIGHZ = 0x02; /// Similarly, sets channel output to high if in totem-pole mode, otherwise -extern const uint8_t PCA9685_MODE_OUTNE_LOW; +inline constexpr uint8_t PCA9685_MODE_OUTNE_LOW = 0x01; class PCA9685Output; From 82cfa00a97a8b6c9d0361c2bec66c3723a2be6e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 15:04:30 -0600 Subject: [PATCH 10/43] [tlc59208f] Make mode constants inline constexpr (#14043) --- esphome/components/tlc59208f/tlc59208f_output.cpp | 12 +----------- esphome/components/tlc59208f/tlc59208f_output.h | 14 +++++++------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/esphome/components/tlc59208f/tlc59208f_output.cpp b/esphome/components/tlc59208f/tlc59208f_output.cpp index 85311a877c..d35585fe5f 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.cpp +++ b/esphome/components/tlc59208f/tlc59208f_output.cpp @@ -26,17 +26,7 @@ const uint8_t TLC59208F_MODE1_SUB3 = (1 << 1); // 0: device doesn't respond to i2c all-call 3, 1*: responds to all-call const uint8_t TLC59208F_MODE1_ALLCALL = (1 << 0); -// 0*: Group dimming, 1: Group blinking -const uint8_t TLC59208F_MODE2_DMBLNK = (1 << 5); -// 0*: Output change on Stop command, 1: Output change on ACK -const uint8_t TLC59208F_MODE2_OCH = (1 << 3); -// 0*: WDT disabled, 1: WDT enabled -const uint8_t TLC59208F_MODE2_WDTEN = (1 << 2); -// WDT timeouts -const uint8_t TLC59208F_MODE2_WDT_5MS = (0 << 0); -const uint8_t TLC59208F_MODE2_WDT_15MS = (1 << 0); -const uint8_t TLC59208F_MODE2_WDT_25MS = (2 << 0); -const uint8_t TLC59208F_MODE2_WDT_35MS = (3 << 0); +// TLC59208F MODE2 constants are now inline constexpr in tlc59208f_output.h // --- Special function --- // Call address to perform software reset, no devices will ACK diff --git a/esphome/components/tlc59208f/tlc59208f_output.h b/esphome/components/tlc59208f/tlc59208f_output.h index 68ca8061d7..34663cd364 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.h +++ b/esphome/components/tlc59208f/tlc59208f_output.h @@ -9,16 +9,16 @@ namespace esphome { namespace tlc59208f { // 0*: Group dimming, 1: Group blinking -extern const uint8_t TLC59208F_MODE2_DMBLNK; +inline constexpr uint8_t TLC59208F_MODE2_DMBLNK = (1 << 5); // 0*: Output change on Stop command, 1: Output change on ACK -extern const uint8_t TLC59208F_MODE2_OCH; +inline constexpr uint8_t TLC59208F_MODE2_OCH = (1 << 3); // 0*: WDT disabled, 1: WDT enabled -extern const uint8_t TLC59208F_MODE2_WDTEN; +inline constexpr uint8_t TLC59208F_MODE2_WDTEN = (1 << 2); // WDT timeouts -extern const uint8_t TLC59208F_MODE2_WDT_5MS; -extern const uint8_t TLC59208F_MODE2_WDT_15MS; -extern const uint8_t TLC59208F_MODE2_WDT_25MS; -extern const uint8_t TLC59208F_MODE2_WDT_35MS; +inline constexpr uint8_t TLC59208F_MODE2_WDT_5MS = (0 << 0); +inline constexpr uint8_t TLC59208F_MODE2_WDT_15MS = (1 << 0); +inline constexpr uint8_t TLC59208F_MODE2_WDT_25MS = (2 << 0); +inline constexpr uint8_t TLC59208F_MODE2_WDT_35MS = (3 << 0); class TLC59208FOutput; From 09fc0288953d13a09bafef3e9038b36d3ff4c1ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 15:16:26 -0600 Subject: [PATCH 11/43] [core] Remove dead global_state variable (#14060) --- esphome/core/component.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index a452f4d400..47c4a70c0f 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -82,8 +82,6 @@ void store_component_error_message(const Component *component, const char *messa const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again -uint32_t global_state = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - float Component::get_loop_priority() const { return 0.0f; } float Component::get_setup_priority() const { return setup_priority::DATA; } From 02e310f2c9549349ae12615f3fec627d9189479a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 16:48:13 -0600 Subject: [PATCH 12/43] [core] Remove unnecessary IRAM_ATTR from yield(), delay(), feed_wdt(), and arch_feed_wdt() (#14063) --- esphome/components/esp32/core.cpp | 6 +++--- esphome/components/esp8266/core.cpp | 6 +++--- esphome/components/host/core.cpp | 6 +++--- esphome/components/libretiny/core.cpp | 6 +++--- esphome/components/rp2040/core.cpp | 6 +++--- esphome/core/application.cpp | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 09a45c14a6..202d929ab9 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -21,9 +21,9 @@ extern "C" __attribute__((weak)) void initArduino() {} namespace esphome { -void IRAM_ATTR HOT yield() { vPortYield(); } +void HOT yield() { vPortYield(); } uint32_t IRAM_ATTR HOT millis() { return (uint32_t) (esp_timer_get_time() / 1000ULL); } -void IRAM_ATTR HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } +void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { @@ -44,7 +44,7 @@ void arch_init() { esp_ota_mark_app_valid_cancel_rollback(); #endif } -void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); } +void HOT arch_feed_wdt() { esp_task_wdt_reset(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 784b87916b..236d3022be 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -14,9 +14,9 @@ extern "C" { namespace esphome { -void IRAM_ATTR HOT yield() { ::yield(); } +void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } +void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { @@ -27,7 +27,7 @@ void arch_restart() { } } void arch_init() {} -void IRAM_ATTR HOT arch_feed_wdt() { system_soft_wdt_feed(); } +void HOT arch_feed_wdt() { system_soft_wdt_feed(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index 164d622dd4..c20a33fa37 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -11,7 +11,7 @@ namespace esphome { -void IRAM_ATTR HOT yield() { ::sched_yield(); } +void HOT yield() { ::sched_yield(); } uint32_t IRAM_ATTR HOT millis() { struct timespec spec; clock_gettime(CLOCK_MONOTONIC, &spec); @@ -19,7 +19,7 @@ uint32_t IRAM_ATTR HOT millis() { uint32_t ms = round(spec.tv_nsec / 1e6); return ((uint32_t) seconds) * 1000U + ms; } -void IRAM_ATTR HOT delay(uint32_t ms) { +void HOT delay(uint32_t ms) { struct timespec ts; ts.tv_sec = ms / 1000; ts.tv_nsec = (ms % 1000) * 1000000; @@ -48,7 +48,7 @@ void arch_restart() { exit(0); } void arch_init() { // pass } -void IRAM_ATTR HOT arch_feed_wdt() { +void HOT arch_feed_wdt() { // pass } diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index b22740f02a..4dda7c3856 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -11,10 +11,10 @@ void loop(); namespace esphome { -void IRAM_ATTR HOT yield() { ::yield(); } +void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } -void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } +void HOT delay(uint32_t ms) { ::delay(ms); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } void arch_init() { @@ -30,7 +30,7 @@ void arch_restart() { while (1) { } } -void IRAM_ATTR HOT arch_feed_wdt() { lt_wdt_feed(); } +void HOT arch_feed_wdt() { lt_wdt_feed(); } uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index d88e9f54b7..37378d88bb 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -9,9 +9,9 @@ namespace esphome { -void IRAM_ATTR HOT yield() { ::yield(); } +void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } +void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { @@ -27,7 +27,7 @@ void arch_init() { #endif } -void IRAM_ATTR HOT arch_feed_wdt() { watchdog_update(); } +void HOT arch_feed_wdt() { watchdog_update(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 449acc64cf..406885fd81 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -240,7 +240,7 @@ void Application::process_dump_config_() { this->dump_config_at_++; } -void IRAM_ATTR HOT Application::feed_wdt(uint32_t time) { +void HOT Application::feed_wdt(uint32_t time) { static uint32_t last_feed = 0; // Use provided time if available, otherwise get current time uint32_t now = time ? time : millis(); From 387f615dae037ad8a19dd6f0fad7e04741eee1d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 16:48:30 -0600 Subject: [PATCH 13/43] [api] Add handshake timeout to prevent connection slot exhaustion (#14050) --- esphome/components/api/api_connection.cpp | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4d564af9e2..5a7994a322 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -60,6 +60,11 @@ static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5; static constexpr uint8_t MAX_PING_RETRIES = 60; static constexpr uint16_t PING_RETRY_INTERVAL = 1000; static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2; +// Timeout for completing the handshake (Noise transport + HelloRequest). +// A stalled handshake from a buggy client or network glitch holds a connection +// slot, which can prevent legitimate clients from reconnecting. Also hardens +// against the less likely case of intentional connection slot exhaustion. +static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000; static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); @@ -205,7 +210,12 @@ void APIConnection::loop() { this->fatal_error_with_log_(LOG_STR("Reading failed"), err); return; } else { - this->last_traffic_ = now; + // Only update last_traffic_ after authentication to ensure the + // handshake timeout is an absolute deadline from connection start. + // Pre-auth messages (e.g. PingRequest) must not reset the timer. + if (this->is_authenticated()) { + this->last_traffic_ = now; + } // read a packet this->read_message(buffer.data_len, buffer.type, buffer.data); if (this->flags_.remove) @@ -223,6 +233,15 @@ void APIConnection::loop() { this->process_active_iterator_(); } + // Disconnect clients that haven't completed the handshake in time. + // Stale half-open connections from buggy clients or network issues can + // accumulate and block legitimate clients from reconnecting. + if (!this->is_authenticated() && now - this->last_traffic_ > HANDSHAKE_TIMEOUT_MS) { + this->on_fatal_error(); + this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("handshake timeout; disconnecting")); + return; + } + if (this->flags_.sent_ping) { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { @@ -1484,6 +1503,8 @@ void APIConnection::complete_authentication_() { } this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); + // Reset traffic timer so keepalive starts from authentication, not connection start + this->last_traffic_ = App.get_loop_component_start_time(); this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected")); #ifdef USE_API_CLIENT_CONNECTED_TRIGGER { From d90754dc0a182785575f2232f3faa29c4d787c84 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 16:49:19 -0600 Subject: [PATCH 14/43] [http_request] Replace heavy STL containers with std::vector for headers (#14024) --- esphome/components/http_request/__init__.py | 2 +- .../components/http_request/http_request.cpp | 22 +++---- .../components/http_request/http_request.h | 58 ++++++++++++------- .../http_request/http_request_arduino.cpp | 12 ++-- .../http_request/http_request_arduino.h | 2 +- .../http_request/http_request_host.cpp | 6 +- .../http_request/http_request_host.h | 2 +- .../http_request/http_request_idf.cpp | 16 ++--- .../http_request/http_request_idf.h | 7 +-- 9 files changed, 65 insertions(+), 62 deletions(-) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 64d74323d6..5faffccbe4 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -310,7 +310,7 @@ async def http_request_action_to_code(config, action_id, template_arg, args): cg.add(var.add_request_header(key, template_)) for value in config.get(CONF_COLLECT_HEADERS, []): - cg.add(var.add_collect_header(value)) + cg.add(var.add_collect_header(value.lower())) if response_conf := config.get(CONF_ON_RESPONSE): if capture_response: diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 11dde4715a..6590d2018e 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -22,23 +22,15 @@ void HttpRequestComponent::dump_config() { } std::string HttpContainer::get_response_header(const std::string &header_name) { - auto response_headers = this->get_response_headers(); - auto header_name_lower_case = str_lower_case(header_name); - if (response_headers.count(header_name_lower_case) == 0) { - ESP_LOGW(TAG, "No header with name %s found", header_name_lower_case.c_str()); - return ""; - } else { - auto values = response_headers[header_name_lower_case]; - if (values.empty()) { - ESP_LOGE(TAG, "header with name %s returned an empty list, this shouldn't happen", - header_name_lower_case.c_str()); - return ""; - } else { - auto header_value = values.front(); - ESP_LOGD(TAG, "Header with name %s found with value %s", header_name_lower_case.c_str(), header_value.c_str()); - return header_value; + auto lower = str_lower_case(header_name); + for (const auto &entry : this->response_headers_) { + if (entry.name == lower) { + ESP_LOGD(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str()); + return entry.value; } } + ESP_LOGW(TAG, "No header with name %s found", lower.c_str()); + return ""; } } // namespace esphome::http_request diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index a427cc4a05..458ffe94a8 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -80,6 +80,16 @@ inline bool is_redirect(int const status) { */ inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; } +/// Check if a header name should be collected (linear scan, fine for small lists) +inline bool should_collect_header(const std::vector &lower_case_collect_headers, + const std::string &lower_header_name) { + for (const auto &h : lower_case_collect_headers) { + if (h == lower_header_name) + return true; + } + return false; +} + /* * HTTP Container Read Semantics * ============================= @@ -258,20 +268,13 @@ class HttpContainer : public Parented { return !this->is_chunked_ && this->bytes_read_ >= this->content_length; } - /** - * @brief Get response headers. - * - * @return The key is the lower case response header name, the value is the header value. - */ - std::map> get_response_headers() { return this->response_headers_; } - std::string get_response_header(const std::string &header_name); protected: size_t bytes_read_{0}; bool secure_{false}; bool is_chunked_{false}; ///< True if response uses chunked transfer encoding - std::map> response_headers_{}; + std::vector
response_headers_{}; }; /// Read data from HTTP container into buffer with timeout handling @@ -333,8 +336,8 @@ class HttpRequestComponent : public Component { return this->start(url, "GET", "", request_headers); } std::shared_ptr get(const std::string &url, const std::list
&request_headers, - const std::set &collect_headers) { - return this->start(url, "GET", "", request_headers, collect_headers); + const std::vector &lower_case_collect_headers) { + return this->start(url, "GET", "", request_headers, lower_case_collect_headers); } std::shared_ptr post(const std::string &url, const std::string &body) { return this->start(url, "POST", body, {}); @@ -345,29 +348,40 @@ class HttpRequestComponent : public Component { } std::shared_ptr post(const std::string &url, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) { - return this->start(url, "POST", body, request_headers, collect_headers); + const std::vector &lower_case_collect_headers) { + return this->start(url, "POST", body, request_headers, lower_case_collect_headers); } std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers) { - return this->start(url, method, body, request_headers, {}); + // Call perform() directly to avoid ambiguity with the std::set overload + return this->perform(url, method, body, request_headers, {}); + } + + // Remove before 2027.1.0 + ESPDEPRECATED("Pass collect_headers as std::vector instead of std::set. Removed in 2027.1.0.", + "2026.7.0") + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::list
&request_headers, + const std::set &collect_headers) { + std::vector lower; + lower.reserve(collect_headers.size()); + for (const auto &h : collect_headers) { + lower.push_back(str_lower_case(h)); + } + return this->perform(url, method, body, request_headers, lower); } std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) { - std::set lower_case_collect_headers; - for (const std::string &collect_header : collect_headers) { - lower_case_collect_headers.insert(str_lower_case(collect_header)); - } + const std::vector &lower_case_collect_headers) { return this->perform(url, method, body, request_headers, lower_case_collect_headers); } protected: virtual std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) = 0; + const std::vector &lower_case_collect_headers) = 0; const char *useragent_{nullptr}; bool follow_redirects_{}; uint16_t redirect_limit_{}; @@ -389,7 +403,7 @@ template class HttpRequestSendAction : public Action { this->request_headers_.insert({key, value}); } - void add_collect_header(const char *value) { this->collect_headers_.insert(value); } + void add_collect_header(const char *value) { this->lower_case_collect_headers_.push_back(value); } void add_json(const char *key, TemplatableValue value) { this->json_.insert({key, value}); } @@ -431,7 +445,7 @@ template class HttpRequestSendAction : public Action { } auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers, - this->collect_headers_); + this->lower_case_collect_headers_); auto captured_args = std::make_tuple(x...); @@ -494,7 +508,7 @@ template class HttpRequestSendAction : public Action { void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); } HttpRequestComponent *parent_; std::map> request_headers_{}; - std::set collect_headers_{"content-type", "content-length"}; + std::vector lower_case_collect_headers_{"content-type", "content-length"}; std::map> json_{}; std::function json_func_{nullptr}; #ifdef USE_HTTP_REQUEST_RESPONSE diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index e5b919e380..3f60b76b58 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -27,7 +27,7 @@ static constexpr int ESP8266_SSL_ERR_OOM = -1000; std::shared_ptr HttpRequestArduino::perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) { + const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); @@ -107,9 +107,9 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur } // returned needed headers must be collected before the requests - const char *header_keys[collect_headers.size()]; + const char *header_keys[lower_case_collect_headers.size()]; int index = 0; - for (auto const &header_name : collect_headers) { + for (auto const &header_name : lower_case_collect_headers) { header_keys[index++] = header_name.c_str(); } container->client_.collectHeaders(header_keys, index); @@ -160,14 +160,14 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur // Still return the container, so it can be used to get the status code and error message } - container->response_headers_ = {}; + container->response_headers_.clear(); auto header_count = container->client_.headers(); for (int i = 0; i < header_count; i++) { const std::string header_name = str_lower_case(container->client_.headerName(i).c_str()); - if (collect_headers.count(header_name) > 0) { + if (should_collect_header(lower_case_collect_headers, header_name)) { std::string header_value = container->client_.header(i).c_str(); ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str()); - container->response_headers_[header_name].push_back(header_value); + container->response_headers_.push_back({header_name, header_value}); } } diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index a1084b12d5..dbd61de364 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -50,7 +50,7 @@ class HttpRequestArduino : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) override; + const std::vector &lower_case_collect_headers) override; }; } // namespace esphome::http_request diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index b94570be12..714a73fc31 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -19,7 +19,7 @@ static const char *const TAG = "http_request.host"; std::shared_ptr HttpRequestHost::perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &response_headers) { + const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); @@ -116,8 +116,8 @@ std::shared_ptr HttpRequestHost::perform(const std::string &url, for (auto header : response.headers) { ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str()); auto lower_name = str_lower_case(header.first); - if (response_headers.find(lower_name) != response_headers.end()) { - container->response_headers_[lower_name].emplace_back(header.second); + if (should_collect_header(lower_case_collect_headers, lower_name)) { + container->response_headers_.push_back({lower_name, header.second}); } } container->duration_ms = millis() - start; diff --git a/esphome/components/http_request/http_request_host.h b/esphome/components/http_request/http_request_host.h index 32e149e6a3..79f5b7e817 100644 --- a/esphome/components/http_request/http_request_host.h +++ b/esphome/components/http_request/http_request_host.h @@ -20,7 +20,7 @@ class HttpRequestHost : public HttpRequestComponent { public: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &response_headers) override; + const std::vector &lower_case_collect_headers) override; void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; } protected: diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 486984a694..0921c50b9f 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -19,8 +19,8 @@ namespace esphome::http_request { static const char *const TAG = "http_request.idf"; struct UserData { - const std::set &collect_headers; - std::map> response_headers; + const std::vector &lower_case_collect_headers; + std::vector
&response_headers; }; void HttpRequestIDF::dump_config() { @@ -38,10 +38,10 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { switch (evt->event_id) { case HTTP_EVENT_ON_HEADER: { const std::string header_name = str_lower_case(evt->header_key); - if (user_data->collect_headers.count(header_name)) { + if (should_collect_header(user_data->lower_case_collect_headers, header_name)) { const std::string header_value = evt->header_value; ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str()); - user_data->response_headers[header_name].push_back(header_value); + user_data->response_headers.push_back({header_name, header_value}); } break; } @@ -55,7 +55,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { std::shared_ptr HttpRequestIDF::perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) { + const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGE(TAG, "HTTP Request failed; Not connected to network"); @@ -110,8 +110,6 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c watchdog::WatchdogManager wdm(this->get_watchdog_timeout()); config.event_handler = http_event_handler; - auto user_data = UserData{collect_headers, {}}; - config.user_data = static_cast(&user_data); esp_http_client_handle_t client = esp_http_client_init(&config); @@ -120,6 +118,9 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c container->set_secure(secure); + auto user_data = UserData{lower_case_collect_headers, container->response_headers_}; + esp_http_client_set_user_data(client, static_cast(&user_data)); + for (const auto &header : request_headers) { esp_http_client_set_header(client, header.name.c_str(), header.value.c_str()); } @@ -164,7 +165,6 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); - container->set_response_headers(user_data.response_headers); container->duration_ms = millis() - start; if (is_success(container->status_code)) { return container; diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index 2a130eae58..9206ba6f5d 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -21,11 +21,8 @@ class HttpContainerIDF : public HttpContainer { /// @brief Feeds the watchdog timer if the executing task has one attached void feed_wdt(); - void set_response_headers(std::map> &response_headers) { - this->response_headers_ = std::move(response_headers); - } - protected: + friend class HttpRequestIDF; esp_http_client_handle_t client_; }; @@ -41,7 +38,7 @@ class HttpRequestIDF : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) override; + const std::vector &lower_case_collect_headers) override; // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE uint16_t buffer_size_rx_{}; uint16_t buffer_size_tx_{}; From bd055e75b9c2def6d9e12170e771920115c9751f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 16:49:37 -0600 Subject: [PATCH 15/43] [core] Shrink Application::dump_config_at_ from size_t to uint16_t (#14053) Co-authored-by: Claude Opus 4.6 --- esphome/core/application.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 30611227a2..e0299f3db3 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -582,9 +582,6 @@ class Application { std::string name_; std::string friendly_name_; - // size_t members - size_t dump_config_at_{SIZE_MAX}; - // 4-byte members uint32_t last_loop_{0}; uint32_t loop_component_start_time_{0}; @@ -594,7 +591,8 @@ class Application { #endif // 2-byte members (grouped together for alignment) - uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds) + uint16_t dump_config_at_{std::numeric_limits::max()}; // Index into components_ for dump_config progress + uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds) uint16_t looping_components_active_end_{0}; // Index marking end of active components in looping_components_ uint16_t current_loop_index_{0}; // For safe reentrant modifications during iteration From 5f82017a310bd60123eadee17a5f2f3d36106694 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:01:00 -0600 Subject: [PATCH 16/43] [udp] Register socket consumption for CONFIG_LWIP_MAX_SOCKETS (#14068) --- esphome/components/udp/__init__.py | 63 ++++++++++++++++++------------ 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index bfaa5f2516..c9586d0b95 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -14,6 +14,7 @@ import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID from esphome.core import ID from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["network"] @@ -65,33 +66,47 @@ RELOCATED = { ) } -CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(UDPComponent), - cv.Optional(CONF_PORT, default=18511): cv.Any( - cv.port, - cv.Schema( + +def _consume_udp_sockets(config: ConfigType) -> ConfigType: + """Register socket needs for UDP component.""" + from esphome.components import socket + + # UDP uses up to 2 sockets: 1 broadcast + 1 listen + # Whether each is used depends on code generation, so register worst case + socket.consume_sockets(2, "udp")(config) + return config + + +CONFIG_SCHEMA = cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(UDPComponent), + cv.Optional(CONF_PORT, default=18511): cv.Any( + cv.port, + cv.Schema( + { + cv.Required(CONF_LISTEN_PORT): cv.port, + cv.Required(CONF_BROADCAST_PORT): cv.port, + } + ), + ), + cv.Optional( + CONF_LISTEN_ADDRESS, default="255.255.255.255" + ): cv.ipv4address_multi_broadcast, + cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( + cv.ipv4address, + ), + cv.Optional(CONF_ON_RECEIVE): automation.validate_automation( { - cv.Required(CONF_LISTEN_PORT): cv.port, - cv.Required(CONF_BROADCAST_PORT): cv.port, + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + Trigger.template(trigger_args) + ), } ), - ), - cv.Optional( - CONF_LISTEN_ADDRESS, default="255.255.255.255" - ): cv.ipv4address_multi_broadcast, - cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( - cv.ipv4address, - ), - cv.Optional(CONF_ON_RECEIVE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Trigger.template(trigger_args) - ), - } - ), - } -).extend(RELOCATED) + } + ).extend(RELOCATED), + _consume_udp_sockets, +) async def register_udp_client(var, config): From 3b869f172064dc994f2ff001e4c73c8da858b01e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:01:37 -0600 Subject: [PATCH 17/43] [web_server] Double socket allocation to prevent connection exhaustion (#14067) --- esphome/components/web_server/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 8b02a6baee..294a5e0a15 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -144,9 +144,10 @@ def _consume_web_server_sockets(config: ConfigType) -> ConfigType: """Register socket needs for web_server component.""" from esphome.components import socket - # Web server needs 1 listening socket + typically 2 concurrent client connections - # (browser makes 2 connections for page + event stream) - sockets_needed = 3 + # Web server needs 1 listening socket + typically 5 concurrent client connections + # (browser opens connections for page resources, SSE event stream, and POST + # requests for entity control which may linger before closing) + sockets_needed = 6 socket.consume_sockets(sockets_needed, "web_server")(config) return config From 565443b710eb8b56bd7bc2d53fef765d195e4303 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:08:53 -0600 Subject: [PATCH 18/43] [pulse_counter] Fix compilation on ESP32-C6/C5/H2/P4 (#14070) --- esphome/components/pulse_counter/pulse_counter_sensor.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index 8ac5a28d8f..ef4cc980f6 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -2,7 +2,7 @@ #include "esphome/core/log.h" #ifdef HAS_PCNT -#include +#include #include #endif @@ -117,9 +117,7 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { } if (this->filter_us != 0) { - uint32_t apb_freq; - esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_APB, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &apb_freq); - uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / apb_freq; + uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / (uint32_t) esp_clk_apb_freq(); pcnt_glitch_filter_config_t filter_config = { .max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns), }; From be853afc2465a291d3a4806122884c52d0bd905e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 20:57:56 -0600 Subject: [PATCH 19/43] [core] Conditionally compile setup_priority override infrastructure (#14057) --- esphome/core/application.cpp | 2 ++ esphome/core/component.cpp | 19 +++++++++++++------ esphome/core/component.h | 1 + esphome/core/defines.h | 1 + esphome/cpp_helpers.py | 3 ++- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 406885fd81..b216233f9b 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -137,8 +137,10 @@ void Application::setup() { ESP_LOGI(TAG, "setup() finished successfully!"); +#ifdef USE_SETUP_PRIORITY_OVERRIDE // Clear setup priority overrides to free memory clear_setup_priority_overrides(); +#endif #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) // Set up wake socket for waking main loop from tasks diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 47c4a70c0f..ba0f1663b9 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -41,20 +41,23 @@ struct ComponentErrorMessage { bool is_flash_ptr; }; +#ifdef USE_SETUP_PRIORITY_OVERRIDE struct ComponentPriorityOverride { const Component *component; float priority; }; +// Setup priority overrides - freed after setup completes +// Using raw pointer instead of unique_ptr to avoid global constructor/destructor overhead +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +std::vector *setup_priority_overrides = nullptr; +#endif + // Error messages for failed components // Using raw pointer instead of unique_ptr to avoid global constructor/destructor overhead // This is never freed as error messages persist for the lifetime of the device // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) std::vector *component_error_messages = nullptr; -// Setup priority overrides - freed after setup completes -// Using raw pointer instead of unique_ptr to avoid global constructor/destructor overhead -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -std::vector *setup_priority_overrides = nullptr; // Helper to store error messages - reduces duplication between deprecated and new API // Remove before 2026.6.0 when deprecated const char* API is removed @@ -459,6 +462,7 @@ void log_update_interval(const char *tag, PollingComponent *component) { } } float Component::get_actual_setup_priority() const { +#ifdef USE_SETUP_PRIORITY_OVERRIDE // Check if there's an override in the global vector if (setup_priority_overrides) { // Linear search is fine for small n (typically < 5 overrides) @@ -468,14 +472,14 @@ float Component::get_actual_setup_priority() const { } } } +#endif return this->get_setup_priority(); } +#ifdef USE_SETUP_PRIORITY_OVERRIDE void Component::set_setup_priority(float priority) { // Lazy allocate the vector if needed if (!setup_priority_overrides) { setup_priority_overrides = new std::vector(); - // Reserve some space to avoid reallocations (most configs have < 10 overrides) - setup_priority_overrides->reserve(10); } // Check if this component already has an override @@ -489,6 +493,7 @@ void Component::set_setup_priority(float priority) { // Add new override setup_priority_overrides->emplace_back(ComponentPriorityOverride{this, priority}); } +#endif bool Component::has_overridden_loop() const { #if defined(USE_HOST) || defined(CLANG_TIDY) @@ -557,10 +562,12 @@ uint32_t WarnIfComponentBlockingGuard::finish() { WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {} +#ifdef USE_SETUP_PRIORITY_OVERRIDE void clear_setup_priority_overrides() { // Free the setup priority map completely delete setup_priority_overrides; setup_priority_overrides = nullptr; } +#endif } // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h index 848bc0ba35..6f7f77dbc1 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -563,6 +563,7 @@ class WarnIfComponentBlockingGuard { }; // Function to clear setup priority overrides after all components are set up +// Only has an implementation when USE_SETUP_PRIORITY_OVERRIDE is defined void clear_setup_priority_overrides(); } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 0d6c1a42e8..bcafcb4c60 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -109,6 +109,7 @@ #define USE_SAFE_MODE_CALLBACK #define USE_SELECT #define USE_SENSOR +#define USE_SETUP_PRIORITY_OVERRIDE #define USE_STATUS_LED #define USE_STATUS_SENSOR #define USE_SWITCH diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 2698b9b3d5..954a28d3d1 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -9,7 +9,7 @@ from esphome.const import ( ) from esphome.core import CORE, ID, coroutine from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import LogStringLiteral, add, get_variable +from esphome.cpp_generator import LogStringLiteral, add, add_define, get_variable from esphome.cpp_types import App from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry @@ -49,6 +49,7 @@ async def register_component(var, config): ) CORE.component_ids.remove(id_) if CONF_SETUP_PRIORITY in config: + add_define("USE_SETUP_PRIORITY_OVERRIDE") add(var.set_setup_priority(config[CONF_SETUP_PRIORITY])) if CONF_UPDATE_INTERVAL in config: add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) From e4c233b6ceef1d1a306e5879752ffc9815d1190b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 20:59:31 -0600 Subject: [PATCH 20/43] [mqtt] Use constexpr for compile-time constants (#14075) --- esphome/components/mqtt/mqtt_backend_esp32.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index adba0cf004..ccc4c4026c 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -114,11 +114,11 @@ struct QueueElement { class MQTTBackendESP32 final : public MQTTBackend { public: - static const size_t MQTT_BUFFER_SIZE = 4096; - static const size_t TASK_STACK_SIZE = 3072; - static const size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations - static const ssize_t TASK_PRIORITY = 5; - static const uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 + static constexpr size_t MQTT_BUFFER_SIZE = 4096; + static constexpr size_t TASK_STACK_SIZE = 3072; + static constexpr size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations + static constexpr ssize_t TASK_PRIORITY = 5; + static constexpr uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; } void set_client_id(const char *client_id) final { this->client_id_ = client_id; } From 66d2ac8cb93a5b7380210ee7933d28620dabd3ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:00:09 -0600 Subject: [PATCH 21/43] [web_server] Move climate static traits to DETAIL_ALL only (#14066) --- esphome/components/web_server/web_server.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 3acd2d2119..a796c1426b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1546,16 +1546,16 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); } + root[ESPHOME_F("max_temp")] = + (value_accuracy_to_buf(temp_buf, traits.get_visual_max_temperature(), target_accuracy), temp_buf); + root[ESPHOME_F("min_temp")] = + (value_accuracy_to_buf(temp_buf, traits.get_visual_min_temperature(), target_accuracy), temp_buf); + root[ESPHOME_F("step")] = traits.get_visual_target_temperature_step(); this->add_sorting_info_(root, obj); } bool has_state = false; root[ESPHOME_F("mode")] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); - root[ESPHOME_F("max_temp")] = - (value_accuracy_to_buf(temp_buf, traits.get_visual_max_temperature(), target_accuracy), temp_buf); - root[ESPHOME_F("min_temp")] = - (value_accuracy_to_buf(temp_buf, traits.get_visual_min_temperature(), target_accuracy), temp_buf); - root[ESPHOME_F("step")] = traits.get_visual_target_temperature_step(); if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { root[ESPHOME_F("action")] = PSTR_LOCAL(climate_action_to_string(obj->action)); root[ESPHOME_F("state")] = root[ESPHOME_F("action")]; From 7e118178b3d5e208eab87e0f882b38dcb18e5ed6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:00:24 -0600 Subject: [PATCH 22/43] [web_server] Fix water_heater JSON key names and move traits to DETAIL_ALL (#14064) --- esphome/components/web_server/web_server.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index a796c1426b..e3d131f58e 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1922,6 +1922,9 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe JsonArray modes = root[ESPHOME_F("modes")].to(); for (auto m : traits.get_supported_modes()) modes.add(PSTR_LOCAL(water_heater::water_heater_mode_to_string(m))); + root[ESPHOME_F("min_temp")] = traits.get_min_temperature(); + root[ESPHOME_F("max_temp")] = traits.get_max_temperature(); + root[ESPHOME_F("step")] = traits.get_target_temperature_step(); this->add_sorting_info_(root, obj); } @@ -1944,10 +1947,6 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe root[ESPHOME_F("target_temperature")] = target; } - root[ESPHOME_F("min_temperature")] = traits.get_min_temperature(); - root[ESPHOME_F("max_temperature")] = traits.get_max_temperature(); - root[ESPHOME_F("step")] = traits.get_target_temperature_step(); - if (traits.get_supports_away_mode()) { root[ESPHOME_F("away")] = obj->is_away(); } From 9c9365c146064177dbc371d425307f8e2cb728d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:07:06 -0600 Subject: [PATCH 23/43] [bluetooth_proxy][esp32_ble_client][esp32_ble_server] Use constexpr for compile-time constants (#14073) --- esphome/components/bluetooth_proxy/bluetooth_proxy.h | 4 ++-- .../components/esp32_ble_client/ble_client_base.cpp | 12 ++++++------ .../components/esp32_ble_server/ble_characteristic.h | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index ab9aee2d81..62a035e79f 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -35,8 +35,8 @@ using namespace esp32_ble_client; // Version 3: New connection API // Version 4: Pairing support // Version 5: Cache clear support -static const uint32_t LEGACY_ACTIVE_CONNECTIONS_VERSION = 5; -static const uint32_t LEGACY_PASSIVE_ONLY_VERSION = 1; +static constexpr uint32_t LEGACY_ACTIVE_CONNECTIONS_VERSION = 5; +static constexpr uint32_t LEGACY_PASSIVE_ONLY_VERSION = 1; enum BluetoothProxyFeature : uint32_t { FEATURE_PASSIVE_SCAN = 1 << 0, diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index c464c89390..3f0eeeab4a 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -16,17 +16,17 @@ static const char *const TAG = "esp32_ble_client"; // Intermediate connection parameters for standard operation // ESP-IDF defaults (12.5-15ms) are too slow for stable connections through WiFi-based BLE proxies, // causing disconnections. These medium parameters balance responsiveness with bandwidth usage. -static const uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x07; // 7 * 1.25ms = 8.75ms -static const uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x09; // 9 * 1.25ms = 11.25ms +static constexpr uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x07; // 7 * 1.25ms = 8.75ms +static constexpr uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x09; // 9 * 1.25ms = 11.25ms // The timeout value was increased from 6s to 8s to address stability issues observed // in certain BLE devices when operating through WiFi-based BLE proxies. The longer // timeout reduces the likelihood of disconnections during periods of high latency. -static const uint16_t MEDIUM_CONN_TIMEOUT = 800; // 800 * 10ms = 8s +static constexpr uint16_t MEDIUM_CONN_TIMEOUT = 800; // 800 * 10ms = 8s // Fastest connection parameters for devices with short discovery timeouts -static const uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum) -static const uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms -static const uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s +static constexpr uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum) +static constexpr uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms +static constexpr uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s static const esp_bt_uuid_t NOTIFY_DESC_UUID = { .len = ESP_UUID_LEN_16, .uuid = diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index c2cdb1660c..72897d1dfb 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -57,12 +57,12 @@ class BLECharacteristic { ESPBTUUID get_uuid() { return this->uuid_; } std::vector &get_value() { return this->value_; } - static const uint32_t PROPERTY_READ = 1 << 0; - static const uint32_t PROPERTY_WRITE = 1 << 1; - static const uint32_t PROPERTY_NOTIFY = 1 << 2; - static const uint32_t PROPERTY_BROADCAST = 1 << 3; - static const uint32_t PROPERTY_INDICATE = 1 << 4; - static const uint32_t PROPERTY_WRITE_NR = 1 << 5; + static constexpr uint32_t PROPERTY_READ = 1 << 0; + static constexpr uint32_t PROPERTY_WRITE = 1 << 1; + static constexpr uint32_t PROPERTY_NOTIFY = 1 << 2; + static constexpr uint32_t PROPERTY_BROADCAST = 1 << 3; + static constexpr uint32_t PROPERTY_INDICATE = 1 << 4; + static constexpr uint32_t PROPERTY_WRITE_NR = 1 << 5; bool is_created(); bool is_failed(); From 76c151c6e6948fbe2f5c62356ef299797cc83480 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:07:38 -0600 Subject: [PATCH 24/43] [api] Use constexpr for compile-time constant (#14072) --- esphome/components/api/api_frame_helper_noise.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 1ae848dead..492988128a 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -474,7 +474,7 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s // buf_start[1], buf_start[2] to be set after encryption // Write message header (to be encrypted) - const uint8_t msg_offset = 3; + constexpr uint8_t msg_offset = 3; buf_start[msg_offset] = static_cast(msg.message_type >> 8); // type high byte buf_start[msg_offset + 1] = static_cast(msg.message_type); // type low byte buf_start[msg_offset + 2] = static_cast(msg.payload_size >> 8); // data_len high byte From ee7d63f73a3587cc86c6d6fe71e5fcd3df4e31b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:09:49 -0600 Subject: [PATCH 25/43] [packet_transport] Use constexpr for compile-time constants (#14074) --- esphome/components/packet_transport/packet_transport.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 365a5f2ec7..7b7a852398 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -58,9 +58,9 @@ union FuData { float f32; }; -static const uint16_t MAGIC_NUMBER = 0x4553; -static const uint16_t MAGIC_PING = 0x5048; -static const uint32_t PREF_HASH = 0x45535043; +static constexpr uint16_t MAGIC_NUMBER = 0x4553; +static constexpr uint16_t MAGIC_PING = 0x5048; +static constexpr uint32_t PREF_HASH = 0x45535043; enum DataKey { ZERO_FILL_KEY, DATA_KEY, From 20239d1bb304ceea6f7056c0466071178ad8af50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:16:09 -0600 Subject: [PATCH 26/43] [remote_base] Use constexpr for compile-time constants (#14076) --- .../remote_base/abbwelcome_protocol.cpp | 8 +++---- .../remote_base/abbwelcome_protocol.h | 4 ++-- .../components/remote_base/aeha_protocol.cpp | 14 +++++------ .../remote_base/byronsx_protocol.cpp | 10 ++++---- .../remote_base/canalsat_protocol.cpp | 8 +++---- .../components/remote_base/dish_protocol.cpp | 10 ++++---- .../components/remote_base/dooya_protocol.cpp | 12 +++++----- .../remote_base/drayton_protocol.cpp | 22 ++++++++--------- .../components/remote_base/jvc_protocol.cpp | 12 +++++----- .../remote_base/keeloq_protocol.cpp | 22 ++++++++--------- .../components/remote_base/lg_protocol.cpp | 10 ++++---- .../remote_base/magiquest_protocol.cpp | 10 ++++---- .../components/remote_base/midea_protocol.h | 2 +- .../components/remote_base/nec_protocol.cpp | 10 ++++---- .../components/remote_base/nexa_protocol.cpp | 22 ++++++++--------- .../remote_base/panasonic_protocol.cpp | 10 ++++---- .../remote_base/pioneer_protocol.cpp | 12 +++++----- .../remote_base/pronto_protocol.cpp | 24 +++++++++---------- .../components/remote_base/rc5_protocol.cpp | 4 ++-- .../components/remote_base/rc6_protocol.cpp | 10 ++++---- .../remote_base/roomba_protocol.cpp | 10 ++++---- .../remote_base/samsung36_protocol.cpp | 20 ++++++++-------- .../remote_base/samsung_protocol.cpp | 14 +++++------ .../components/remote_base/sony_protocol.cpp | 10 ++++---- .../remote_base/symphony_protocol.cpp | 14 +++++------ .../remote_base/toshiba_ac_protocol.cpp | 16 ++++++------- .../components/remote_base/toto_protocol.cpp | 12 +++++----- 27 files changed, 166 insertions(+), 166 deletions(-) diff --git a/esphome/components/remote_base/abbwelcome_protocol.cpp b/esphome/components/remote_base/abbwelcome_protocol.cpp index 352ae10ed7..a67ca48dbe 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.cpp +++ b/esphome/components/remote_base/abbwelcome_protocol.cpp @@ -6,10 +6,10 @@ namespace remote_base { static const char *const TAG = "remote.abbwelcome"; -static const uint32_t BIT_ONE_SPACE_US = 102; -static const uint32_t BIT_ZERO_MARK_US = 32; // 18-44 -static const uint32_t BIT_ZERO_SPACE_US = BIT_ONE_SPACE_US - BIT_ZERO_MARK_US; -static const uint16_t BYTE_SPACE_US = 210; +static constexpr uint32_t BIT_ONE_SPACE_US = 102; +static constexpr uint32_t BIT_ZERO_MARK_US = 32; // 18-44 +static constexpr uint32_t BIT_ZERO_SPACE_US = BIT_ONE_SPACE_US - BIT_ZERO_MARK_US; +static constexpr uint16_t BYTE_SPACE_US = 210; uint8_t ABBWelcomeData::calc_cs_() const { uint8_t checksum = 0; diff --git a/esphome/components/remote_base/abbwelcome_protocol.h b/esphome/components/remote_base/abbwelcome_protocol.h index 1dddedf8ce..66664a89f3 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.h +++ b/esphome/components/remote_base/abbwelcome_protocol.h @@ -11,8 +11,8 @@ namespace esphome { namespace remote_base { -static const uint8_t MAX_DATA_LENGTH = 15; -static const uint8_t DATA_LENGTH_MASK = 0x3f; +static constexpr uint8_t MAX_DATA_LENGTH = 15; +static constexpr uint8_t DATA_LENGTH_MASK = 0x3f; /* Message Format: diff --git a/esphome/components/remote_base/aeha_protocol.cpp b/esphome/components/remote_base/aeha_protocol.cpp index 3b926e7981..f40cff7623 100644 --- a/esphome/components/remote_base/aeha_protocol.cpp +++ b/esphome/components/remote_base/aeha_protocol.cpp @@ -7,13 +7,13 @@ namespace remote_base { static const char *const TAG = "remote.aeha"; -static const uint16_t BITWISE = 425; -static const uint16_t HEADER_HIGH_US = BITWISE * 8; -static const uint16_t HEADER_LOW_US = BITWISE * 4; -static const uint16_t BIT_HIGH_US = BITWISE; -static const uint16_t BIT_ONE_LOW_US = BITWISE * 3; -static const uint16_t BIT_ZERO_LOW_US = BITWISE; -static const uint16_t TRAILER = BITWISE; +static constexpr uint16_t BITWISE = 425; +static constexpr uint16_t HEADER_HIGH_US = BITWISE * 8; +static constexpr uint16_t HEADER_LOW_US = BITWISE * 4; +static constexpr uint16_t BIT_HIGH_US = BITWISE; +static constexpr uint16_t BIT_ONE_LOW_US = BITWISE * 3; +static constexpr uint16_t BIT_ZERO_LOW_US = BITWISE; +static constexpr uint16_t TRAILER = BITWISE; void AEHAProtocol::encode(RemoteTransmitData *dst, const AEHAData &data) { dst->reserve(2 + 32 + (data.data.size() * 2) + 1); diff --git a/esphome/components/remote_base/byronsx_protocol.cpp b/esphome/components/remote_base/byronsx_protocol.cpp index 6bfa4b7ff9..f243b5bdfd 100644 --- a/esphome/components/remote_base/byronsx_protocol.cpp +++ b/esphome/components/remote_base/byronsx_protocol.cpp @@ -8,11 +8,11 @@ namespace remote_base { static const char *const TAG = "remote.byronsx"; -static const uint32_t BIT_TIME_US = 333; -static const uint8_t NBITS_ADDRESS = 8; -static const uint8_t NBITS_COMMAND = 4; -static const uint8_t NBITS_START_BIT = 1; -static const uint8_t NBITS_DATA = NBITS_ADDRESS + NBITS_COMMAND /*+ NBITS_COMMAND*/; +static constexpr uint32_t BIT_TIME_US = 333; +static constexpr uint8_t NBITS_ADDRESS = 8; +static constexpr uint8_t NBITS_COMMAND = 4; +static constexpr uint8_t NBITS_START_BIT = 1; +static constexpr uint8_t NBITS_DATA = NBITS_ADDRESS + NBITS_COMMAND /*+ NBITS_COMMAND*/; /* ByronSX Protocol diff --git a/esphome/components/remote_base/canalsat_protocol.cpp b/esphome/components/remote_base/canalsat_protocol.cpp index bee3d57fd8..1468b66939 100644 --- a/esphome/components/remote_base/canalsat_protocol.cpp +++ b/esphome/components/remote_base/canalsat_protocol.cpp @@ -7,10 +7,10 @@ namespace remote_base { static const char *const CANALSAT_TAG = "remote.canalsat"; static const char *const CANALSATLD_TAG = "remote.canalsatld"; -static const uint16_t CANALSAT_FREQ = 55500; -static const uint16_t CANALSATLD_FREQ = 56000; -static const uint16_t CANALSAT_UNIT = 250; -static const uint16_t CANALSATLD_UNIT = 320; +static constexpr uint16_t CANALSAT_FREQ = 55500; +static constexpr uint16_t CANALSATLD_FREQ = 56000; +static constexpr uint16_t CANALSAT_UNIT = 250; +static constexpr uint16_t CANALSATLD_UNIT = 320; CanalSatProtocol::CanalSatProtocol() { this->frequency_ = CANALSAT_FREQ; diff --git a/esphome/components/remote_base/dish_protocol.cpp b/esphome/components/remote_base/dish_protocol.cpp index 754b6c3b12..69226101bf 100644 --- a/esphome/components/remote_base/dish_protocol.cpp +++ b/esphome/components/remote_base/dish_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.dish"; -static const uint32_t HEADER_HIGH_US = 400; -static const uint32_t HEADER_LOW_US = 6100; -static const uint32_t BIT_HIGH_US = 400; -static const uint32_t BIT_ONE_LOW_US = 1700; -static const uint32_t BIT_ZERO_LOW_US = 2800; +static constexpr uint32_t HEADER_HIGH_US = 400; +static constexpr uint32_t HEADER_LOW_US = 6100; +static constexpr uint32_t BIT_HIGH_US = 400; +static constexpr uint32_t BIT_ONE_LOW_US = 1700; +static constexpr uint32_t BIT_ZERO_LOW_US = 2800; void DishProtocol::encode(RemoteTransmitData *dst, const DishData &data) { dst->reserve(138); diff --git a/esphome/components/remote_base/dooya_protocol.cpp b/esphome/components/remote_base/dooya_protocol.cpp index 04c5fef8f3..84bdbf3e08 100644 --- a/esphome/components/remote_base/dooya_protocol.cpp +++ b/esphome/components/remote_base/dooya_protocol.cpp @@ -6,12 +6,12 @@ namespace remote_base { static const char *const TAG = "remote.dooya"; -static const uint32_t HEADER_HIGH_US = 5000; -static const uint32_t HEADER_LOW_US = 1500; -static const uint32_t BIT_ZERO_HIGH_US = 350; -static const uint32_t BIT_ZERO_LOW_US = 750; -static const uint32_t BIT_ONE_HIGH_US = 750; -static const uint32_t BIT_ONE_LOW_US = 350; +static constexpr uint32_t HEADER_HIGH_US = 5000; +static constexpr uint32_t HEADER_LOW_US = 1500; +static constexpr uint32_t BIT_ZERO_HIGH_US = 350; +static constexpr uint32_t BIT_ZERO_LOW_US = 750; +static constexpr uint32_t BIT_ONE_HIGH_US = 750; +static constexpr uint32_t BIT_ONE_LOW_US = 350; void DooyaProtocol::encode(RemoteTransmitData *dst, const DooyaData &data) { dst->set_carrier_frequency(0); diff --git a/esphome/components/remote_base/drayton_protocol.cpp b/esphome/components/remote_base/drayton_protocol.cpp index da2e985af0..946bd9cacb 100644 --- a/esphome/components/remote_base/drayton_protocol.cpp +++ b/esphome/components/remote_base/drayton_protocol.cpp @@ -8,18 +8,18 @@ namespace remote_base { static const char *const TAG = "remote.drayton"; -static const uint32_t BIT_TIME_US = 500; -static const uint8_t CARRIER_KHZ = 2; -static const uint8_t NBITS_PREAMBLE = 12; -static const uint8_t NBITS_SYNC = 4; -static const uint8_t NBITS_ADDRESS = 16; -static const uint8_t NBITS_CHANNEL = 5; -static const uint8_t NBITS_COMMAND = 7; -static const uint8_t NDATABITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND; -static const uint8_t MIN_RX_SRC = (NDATABITS + NBITS_SYNC / 2); +static constexpr uint32_t BIT_TIME_US = 500; +static constexpr uint8_t CARRIER_KHZ = 2; +static constexpr uint8_t NBITS_PREAMBLE = 12; +static constexpr uint8_t NBITS_SYNC = 4; +static constexpr uint8_t NBITS_ADDRESS = 16; +static constexpr uint8_t NBITS_CHANNEL = 5; +static constexpr uint8_t NBITS_COMMAND = 7; +static constexpr uint8_t NDATABITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND; +static constexpr uint8_t MIN_RX_SRC = (NDATABITS + NBITS_SYNC / 2); -static const uint8_t CMD_ON = 0x41; -static const uint8_t CMD_OFF = 0x02; +static constexpr uint8_t CMD_ON = 0x41; +static constexpr uint8_t CMD_OFF = 0x02; /* Drayton Protocol diff --git a/esphome/components/remote_base/jvc_protocol.cpp b/esphome/components/remote_base/jvc_protocol.cpp index ca423b61e6..c33cae7a48 100644 --- a/esphome/components/remote_base/jvc_protocol.cpp +++ b/esphome/components/remote_base/jvc_protocol.cpp @@ -6,12 +6,12 @@ namespace remote_base { static const char *const TAG = "remote.jvc"; -static const uint8_t NBITS = 16; -static const uint32_t HEADER_HIGH_US = 8400; -static const uint32_t HEADER_LOW_US = 4200; -static const uint32_t BIT_ONE_LOW_US = 1725; -static const uint32_t BIT_ZERO_LOW_US = 525; -static const uint32_t BIT_HIGH_US = 525; +static constexpr uint8_t NBITS = 16; +static constexpr uint32_t HEADER_HIGH_US = 8400; +static constexpr uint32_t HEADER_LOW_US = 4200; +static constexpr uint32_t BIT_ONE_LOW_US = 1725; +static constexpr uint32_t BIT_ZERO_LOW_US = 525; +static constexpr uint32_t BIT_HIGH_US = 525; void JVCProtocol::encode(RemoteTransmitData *dst, const JVCData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/keeloq_protocol.cpp b/esphome/components/remote_base/keeloq_protocol.cpp index 72540c37f1..e95c79ef25 100644 --- a/esphome/components/remote_base/keeloq_protocol.cpp +++ b/esphome/components/remote_base/keeloq_protocol.cpp @@ -8,18 +8,18 @@ namespace remote_base { static const char *const TAG = "remote.keeloq"; -static const uint32_t BIT_TIME_US = 380; -static const uint8_t NBITS_PREAMBLE = 12; -static const uint8_t NBITS_REPEAT = 1; -static const uint8_t NBITS_VLOW = 1; -static const uint8_t NBITS_SERIAL = 28; -static const uint8_t NBITS_BUTTONS = 4; -static const uint8_t NBITS_DISC = 12; -static const uint8_t NBITS_SYNC_CNT = 16; +static constexpr uint32_t BIT_TIME_US = 380; +static constexpr uint8_t NBITS_PREAMBLE = 12; +static constexpr uint8_t NBITS_REPEAT = 1; +static constexpr uint8_t NBITS_VLOW = 1; +static constexpr uint8_t NBITS_SERIAL = 28; +static constexpr uint8_t NBITS_BUTTONS = 4; +static constexpr uint8_t NBITS_DISC = 12; +static constexpr uint8_t NBITS_SYNC_CNT = 16; -static const uint8_t NBITS_FIXED_DATA = NBITS_REPEAT + NBITS_VLOW + NBITS_BUTTONS + NBITS_SERIAL; -static const uint8_t NBITS_ENCRYPTED_DATA = NBITS_BUTTONS + NBITS_DISC + NBITS_SYNC_CNT; -static const uint8_t NBITS_DATA = NBITS_FIXED_DATA + NBITS_ENCRYPTED_DATA; +static constexpr uint8_t NBITS_FIXED_DATA = NBITS_REPEAT + NBITS_VLOW + NBITS_BUTTONS + NBITS_SERIAL; +static constexpr uint8_t NBITS_ENCRYPTED_DATA = NBITS_BUTTONS + NBITS_DISC + NBITS_SYNC_CNT; +static constexpr uint8_t NBITS_DATA = NBITS_FIXED_DATA + NBITS_ENCRYPTED_DATA; /* KeeLoq Protocol diff --git a/esphome/components/remote_base/lg_protocol.cpp b/esphome/components/remote_base/lg_protocol.cpp index d25c59d2b1..4c54ff00bd 100644 --- a/esphome/components/remote_base/lg_protocol.cpp +++ b/esphome/components/remote_base/lg_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.lg"; -static const uint32_t HEADER_HIGH_US = 8000; -static const uint32_t HEADER_LOW_US = 4000; -static const uint32_t BIT_HIGH_US = 600; -static const uint32_t BIT_ONE_LOW_US = 1600; -static const uint32_t BIT_ZERO_LOW_US = 550; +static constexpr uint32_t HEADER_HIGH_US = 8000; +static constexpr uint32_t HEADER_LOW_US = 4000; +static constexpr uint32_t BIT_HIGH_US = 600; +static constexpr uint32_t BIT_ONE_LOW_US = 1600; +static constexpr uint32_t BIT_ZERO_LOW_US = 550; void LGProtocol::encode(RemoteTransmitData *dst, const LGData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/magiquest_protocol.cpp b/esphome/components/remote_base/magiquest_protocol.cpp index 1cec58a55f..f25a982fdc 100644 --- a/esphome/components/remote_base/magiquest_protocol.cpp +++ b/esphome/components/remote_base/magiquest_protocol.cpp @@ -10,11 +10,11 @@ namespace remote_base { static const char *const TAG = "remote.magiquest"; -static const uint32_t MAGIQUEST_UNIT = 288; // us -static const uint32_t MAGIQUEST_ONE_MARK = 2 * MAGIQUEST_UNIT; -static const uint32_t MAGIQUEST_ONE_SPACE = 2 * MAGIQUEST_UNIT; -static const uint32_t MAGIQUEST_ZERO_MARK = MAGIQUEST_UNIT; -static const uint32_t MAGIQUEST_ZERO_SPACE = 3 * MAGIQUEST_UNIT; +static constexpr uint32_t MAGIQUEST_UNIT = 288; // us +static constexpr uint32_t MAGIQUEST_ONE_MARK = 2 * MAGIQUEST_UNIT; +static constexpr uint32_t MAGIQUEST_ONE_SPACE = 2 * MAGIQUEST_UNIT; +static constexpr uint32_t MAGIQUEST_ZERO_MARK = MAGIQUEST_UNIT; +static constexpr uint32_t MAGIQUEST_ZERO_SPACE = 3 * MAGIQUEST_UNIT; void MagiQuestProtocol::encode(RemoteTransmitData *dst, const MagiQuestData &data) { dst->reserve(101); // 2 start bits, 48 data bits, 1 stop bit diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h index ddefff867a..334e8a7cb3 100644 --- a/esphome/components/remote_base/midea_protocol.h +++ b/esphome/components/remote_base/midea_protocol.h @@ -62,7 +62,7 @@ class MideaData { this->data_[idx] |= (value << shift); } void set_mask_(uint8_t idx, bool state, uint8_t mask = 255) { this->set_value_(idx, state ? mask : 0, mask); } - static const uint8_t OFFSET_CS = 5; + static constexpr uint8_t OFFSET_CS = 5; // 48-bits data std::array data_; // Calculate checksum diff --git a/esphome/components/remote_base/nec_protocol.cpp b/esphome/components/remote_base/nec_protocol.cpp index 6ea9a8583c..062f81b4d6 100644 --- a/esphome/components/remote_base/nec_protocol.cpp +++ b/esphome/components/remote_base/nec_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.nec"; -static const uint32_t HEADER_HIGH_US = 9000; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 560; -static const uint32_t BIT_ONE_LOW_US = 1690; -static const uint32_t BIT_ZERO_LOW_US = 560; +static constexpr uint32_t HEADER_HIGH_US = 9000; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 560; +static constexpr uint32_t BIT_ONE_LOW_US = 1690; +static constexpr uint32_t BIT_ZERO_LOW_US = 560; void NECProtocol::encode(RemoteTransmitData *dst, const NECData &data) { ESP_LOGD(TAG, "Sending NEC: address=0x%04X, command=0x%04X command_repeats=%d", data.address, data.command, diff --git a/esphome/components/remote_base/nexa_protocol.cpp b/esphome/components/remote_base/nexa_protocol.cpp index 862165714e..28b415d4d6 100644 --- a/esphome/components/remote_base/nexa_protocol.cpp +++ b/esphome/components/remote_base/nexa_protocol.cpp @@ -6,18 +6,18 @@ namespace remote_base { static const char *const TAG = "remote.nexa"; -static const uint8_t NBITS = 32; -static const uint32_t HEADER_HIGH_US = 319; -static const uint32_t HEADER_LOW_US = 2610; -static const uint32_t BIT_HIGH_US = 319; -static const uint32_t BIT_ONE_LOW_US = 1000; -static const uint32_t BIT_ZERO_LOW_US = 140; +static constexpr uint8_t NBITS = 32; +static constexpr uint32_t HEADER_HIGH_US = 319; +static constexpr uint32_t HEADER_LOW_US = 2610; +static constexpr uint32_t BIT_HIGH_US = 319; +static constexpr uint32_t BIT_ONE_LOW_US = 1000; +static constexpr uint32_t BIT_ZERO_LOW_US = 140; -static const uint32_t TX_HEADER_HIGH_US = 250; -static const uint32_t TX_HEADER_LOW_US = TX_HEADER_HIGH_US * 10; -static const uint32_t TX_BIT_HIGH_US = 250; -static const uint32_t TX_BIT_ONE_LOW_US = TX_BIT_HIGH_US * 5; -static const uint32_t TX_BIT_ZERO_LOW_US = TX_BIT_HIGH_US * 1; +static constexpr uint32_t TX_HEADER_HIGH_US = 250; +static constexpr uint32_t TX_HEADER_LOW_US = TX_HEADER_HIGH_US * 10; +static constexpr uint32_t TX_BIT_HIGH_US = 250; +static constexpr uint32_t TX_BIT_ONE_LOW_US = TX_BIT_HIGH_US * 5; +static constexpr uint32_t TX_BIT_ZERO_LOW_US = TX_BIT_HIGH_US * 1; void NexaProtocol::one(RemoteTransmitData *dst) const { // '1' => '10' diff --git a/esphome/components/remote_base/panasonic_protocol.cpp b/esphome/components/remote_base/panasonic_protocol.cpp index d6cf1a160d..e0acc42692 100644 --- a/esphome/components/remote_base/panasonic_protocol.cpp +++ b/esphome/components/remote_base/panasonic_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.panasonic"; -static const uint32_t HEADER_HIGH_US = 3502; -static const uint32_t HEADER_LOW_US = 1750; -static const uint32_t BIT_HIGH_US = 502; -static const uint32_t BIT_ZERO_LOW_US = 400; -static const uint32_t BIT_ONE_LOW_US = 1244; +static constexpr uint32_t HEADER_HIGH_US = 3502; +static constexpr uint32_t HEADER_LOW_US = 1750; +static constexpr uint32_t BIT_HIGH_US = 502; +static constexpr uint32_t BIT_ZERO_LOW_US = 400; +static constexpr uint32_t BIT_ONE_LOW_US = 1244; void PanasonicProtocol::encode(RemoteTransmitData *dst, const PanasonicData &data) { dst->reserve(100); diff --git a/esphome/components/remote_base/pioneer_protocol.cpp b/esphome/components/remote_base/pioneer_protocol.cpp index 043565282d..f350ef66ae 100644 --- a/esphome/components/remote_base/pioneer_protocol.cpp +++ b/esphome/components/remote_base/pioneer_protocol.cpp @@ -6,12 +6,12 @@ namespace remote_base { static const char *const TAG = "remote.pioneer"; -static const uint32_t HEADER_HIGH_US = 9000; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 560; -static const uint32_t BIT_ONE_LOW_US = 1690; -static const uint32_t BIT_ZERO_LOW_US = 560; -static const uint32_t TRAILER_SPACE_US = 25500; +static constexpr uint32_t HEADER_HIGH_US = 9000; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 560; +static constexpr uint32_t BIT_ONE_LOW_US = 1690; +static constexpr uint32_t BIT_ZERO_LOW_US = 560; +static constexpr uint32_t TRAILER_SPACE_US = 25500; void PioneerProtocol::encode(RemoteTransmitData *dst, const PioneerData &data) { uint32_t address1 = ((data.rc_code_1 & 0xff00) | (~(data.rc_code_1 >> 8) & 0xff)); diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 401a0976b2..cff3145199 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -59,18 +59,18 @@ bool ProntoData::operator==(const ProntoData &rhs) const { } // DO NOT EXPORT from this file -static const uint16_t MICROSECONDS_T_MAX = 0xFFFFU; -static const uint16_t LEARNED_TOKEN = 0x0000U; -static const uint16_t LEARNED_NON_MODULATED_TOKEN = 0x0100U; -static const uint16_t BITS_IN_HEXADECIMAL = 4U; -static const uint16_t DIGITS_IN_PRONTO_NUMBER = 4U; -static const uint16_t NUMBERS_IN_PREAMBLE = 4U; -static const uint16_t HEX_MASK = 0xFU; -static const uint32_t REFERENCE_FREQUENCY = 4145146UL; -static const uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0; -static const uint32_t MICROSECONDS_IN_SECONDS = 1000000UL; -static const uint16_t PRONTO_DEFAULT_GAP = 45000; -static const uint16_t MARK_EXCESS_MICROS = 20; +static constexpr uint16_t MICROSECONDS_T_MAX = 0xFFFFU; +static constexpr uint16_t LEARNED_TOKEN = 0x0000U; +static constexpr uint16_t LEARNED_NON_MODULATED_TOKEN = 0x0100U; +static constexpr uint16_t BITS_IN_HEXADECIMAL = 4U; +static constexpr uint16_t DIGITS_IN_PRONTO_NUMBER = 4U; +static constexpr uint16_t NUMBERS_IN_PREAMBLE = 4U; +static constexpr uint16_t HEX_MASK = 0xFU; +static constexpr uint32_t REFERENCE_FREQUENCY = 4145146UL; +static constexpr uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0; +static constexpr uint32_t MICROSECONDS_IN_SECONDS = 1000000UL; +static constexpr uint16_t PRONTO_DEFAULT_GAP = 45000; +static constexpr uint16_t MARK_EXCESS_MICROS = 20; static constexpr size_t PRONTO_LOG_CHUNK_SIZE = 230; static uint16_t to_frequency_k_hz(uint16_t code) { diff --git a/esphome/components/remote_base/rc5_protocol.cpp b/esphome/components/remote_base/rc5_protocol.cpp index 08f2f2eaa3..bb6d382d80 100644 --- a/esphome/components/remote_base/rc5_protocol.cpp +++ b/esphome/components/remote_base/rc5_protocol.cpp @@ -6,8 +6,8 @@ namespace remote_base { static const char *const TAG = "remote.rc5"; -static const uint32_t BIT_TIME_US = 889; -static const uint8_t NBITS = 14; +static constexpr uint32_t BIT_TIME_US = 889; +static constexpr uint8_t NBITS = 14; void RC5Protocol::encode(RemoteTransmitData *dst, const RC5Data &data) { static bool toggle = false; diff --git a/esphome/components/remote_base/rc6_protocol.cpp b/esphome/components/remote_base/rc6_protocol.cpp index fcb4da11a4..b442bb4c27 100644 --- a/esphome/components/remote_base/rc6_protocol.cpp +++ b/esphome/components/remote_base/rc6_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const RC6_TAG = "remote.rc6"; -static const uint16_t RC6_FREQ = 36000; -static const uint16_t RC6_UNIT = 444; -static const uint16_t RC6_HEADER_MARK = (6 * RC6_UNIT); -static const uint16_t RC6_HEADER_SPACE = (2 * RC6_UNIT); -static const uint16_t RC6_MODE_MASK = 0x07; +static constexpr uint16_t RC6_FREQ = 36000; +static constexpr uint16_t RC6_UNIT = 444; +static constexpr uint16_t RC6_HEADER_MARK = (6 * RC6_UNIT); +static constexpr uint16_t RC6_HEADER_SPACE = (2 * RC6_UNIT); +static constexpr uint16_t RC6_MODE_MASK = 0x07; void RC6Protocol::encode(RemoteTransmitData *dst, const RC6Data &data) { dst->reserve(44); diff --git a/esphome/components/remote_base/roomba_protocol.cpp b/esphome/components/remote_base/roomba_protocol.cpp index 2d2dde114a..6b7d216374 100644 --- a/esphome/components/remote_base/roomba_protocol.cpp +++ b/esphome/components/remote_base/roomba_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.roomba"; -static const uint8_t NBITS = 8; -static const uint32_t BIT_ONE_HIGH_US = 3000; -static const uint32_t BIT_ONE_LOW_US = 1000; -static const uint32_t BIT_ZERO_HIGH_US = BIT_ONE_LOW_US; -static const uint32_t BIT_ZERO_LOW_US = BIT_ONE_HIGH_US; +static constexpr uint8_t NBITS = 8; +static constexpr uint32_t BIT_ONE_HIGH_US = 3000; +static constexpr uint32_t BIT_ONE_LOW_US = 1000; +static constexpr uint32_t BIT_ZERO_HIGH_US = BIT_ONE_LOW_US; +static constexpr uint32_t BIT_ZERO_LOW_US = BIT_ONE_HIGH_US; void RoombaProtocol::encode(RemoteTransmitData *dst, const RoombaData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/samsung36_protocol.cpp b/esphome/components/remote_base/samsung36_protocol.cpp index bd3eee5849..10e8bd2d01 100644 --- a/esphome/components/remote_base/samsung36_protocol.cpp +++ b/esphome/components/remote_base/samsung36_protocol.cpp @@ -6,17 +6,17 @@ namespace remote_base { static const char *const TAG = "remote.samsung36"; -static const uint8_t NBITS = 78; +static constexpr uint8_t NBITS = 78; -static const uint32_t HEADER_HIGH_US = 4500; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 500; -static const uint32_t BIT_ONE_LOW_US = 1500; -static const uint32_t BIT_ZERO_LOW_US = 500; -static const uint32_t MIDDLE_HIGH_US = 500; -static const uint32_t MIDDLE_LOW_US = 4500; -static const uint32_t FOOTER_HIGH_US = 500; -static const uint32_t FOOTER_LOW_US = 59000; +static constexpr uint32_t HEADER_HIGH_US = 4500; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 500; +static constexpr uint32_t BIT_ONE_LOW_US = 1500; +static constexpr uint32_t BIT_ZERO_LOW_US = 500; +static constexpr uint32_t MIDDLE_HIGH_US = 500; +static constexpr uint32_t MIDDLE_LOW_US = 4500; +static constexpr uint32_t FOOTER_HIGH_US = 500; +static constexpr uint32_t FOOTER_LOW_US = 59000; void Samsung36Protocol::encode(RemoteTransmitData *dst, const Samsung36Data &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/samsung_protocol.cpp b/esphome/components/remote_base/samsung_protocol.cpp index 2d6d5531e5..2a48cbb918 100644 --- a/esphome/components/remote_base/samsung_protocol.cpp +++ b/esphome/components/remote_base/samsung_protocol.cpp @@ -7,13 +7,13 @@ namespace remote_base { static const char *const TAG = "remote.samsung"; -static const uint32_t HEADER_HIGH_US = 4500; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 560; -static const uint32_t BIT_ONE_LOW_US = 1690; -static const uint32_t BIT_ZERO_LOW_US = 560; -static const uint32_t FOOTER_HIGH_US = 560; -static const uint32_t FOOTER_LOW_US = 560; +static constexpr uint32_t HEADER_HIGH_US = 4500; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 560; +static constexpr uint32_t BIT_ONE_LOW_US = 1690; +static constexpr uint32_t BIT_ZERO_LOW_US = 560; +static constexpr uint32_t FOOTER_HIGH_US = 560; +static constexpr uint32_t FOOTER_LOW_US = 560; void SamsungProtocol::encode(RemoteTransmitData *dst, const SamsungData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/sony_protocol.cpp b/esphome/components/remote_base/sony_protocol.cpp index 69f2b49c42..504b346925 100644 --- a/esphome/components/remote_base/sony_protocol.cpp +++ b/esphome/components/remote_base/sony_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.sony"; -static const uint32_t HEADER_HIGH_US = 2400; -static const uint32_t HEADER_LOW_US = 600; -static const uint32_t BIT_ONE_HIGH_US = 1200; -static const uint32_t BIT_ZERO_HIGH_US = 600; -static const uint32_t BIT_LOW_US = 600; +static constexpr uint32_t HEADER_HIGH_US = 2400; +static constexpr uint32_t HEADER_LOW_US = 600; +static constexpr uint32_t BIT_ONE_HIGH_US = 1200; +static constexpr uint32_t BIT_ZERO_HIGH_US = 600; +static constexpr uint32_t BIT_LOW_US = 600; void SonyProtocol::encode(RemoteTransmitData *dst, const SonyData &data) { dst->set_carrier_frequency(40000); diff --git a/esphome/components/remote_base/symphony_protocol.cpp b/esphome/components/remote_base/symphony_protocol.cpp index 34b5dba07f..f30a980d91 100644 --- a/esphome/components/remote_base/symphony_protocol.cpp +++ b/esphome/components/remote_base/symphony_protocol.cpp @@ -13,16 +13,16 @@ static const char *const TAG = "remote.symphony"; // footer-gap handling used there. // Symphony protocol timing specifications (tuned to handset captures) -static const uint32_t BIT_ZERO_HIGH_US = 460; // short -static const uint32_t BIT_ZERO_LOW_US = 1260; // long -static const uint32_t BIT_ONE_HIGH_US = 1260; // long -static const uint32_t BIT_ONE_LOW_US = 460; // short -static const uint32_t CARRIER_FREQUENCY = 38000; +static constexpr uint32_t BIT_ZERO_HIGH_US = 460; // short +static constexpr uint32_t BIT_ZERO_LOW_US = 1260; // long +static constexpr uint32_t BIT_ONE_HIGH_US = 1260; // long +static constexpr uint32_t BIT_ONE_LOW_US = 460; // short +static constexpr uint32_t CARRIER_FREQUENCY = 38000; // IRremoteESP8266 reference: kSymphonyFooterGap = 4 * (mark + space) -static const uint32_t FOOTER_GAP_US = 4 * (BIT_ZERO_HIGH_US + BIT_ZERO_LOW_US); +static constexpr uint32_t FOOTER_GAP_US = 4 * (BIT_ZERO_HIGH_US + BIT_ZERO_LOW_US); // Typical inter-frame gap (~34.8 ms observed) -static const uint32_t INTER_FRAME_GAP_US = 34760; +static constexpr uint32_t INTER_FRAME_GAP_US = 34760; void SymphonyProtocol::encode(RemoteTransmitData *dst, const SymphonyData &data) { dst->set_carrier_frequency(CARRIER_FREQUENCY); diff --git a/esphome/components/remote_base/toshiba_ac_protocol.cpp b/esphome/components/remote_base/toshiba_ac_protocol.cpp index 42241eea8c..a20a29b84a 100644 --- a/esphome/components/remote_base/toshiba_ac_protocol.cpp +++ b/esphome/components/remote_base/toshiba_ac_protocol.cpp @@ -7,14 +7,14 @@ namespace remote_base { static const char *const TAG = "remote.toshibaac"; -static const uint32_t HEADER_HIGH_US = 4500; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 560; -static const uint32_t BIT_ONE_LOW_US = 1690; -static const uint32_t BIT_ZERO_LOW_US = 560; -static const uint32_t FOOTER_HIGH_US = 560; -static const uint32_t FOOTER_LOW_US = 4500; -static const uint16_t PACKET_SPACE = 5500; +static constexpr uint32_t HEADER_HIGH_US = 4500; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 560; +static constexpr uint32_t BIT_ONE_LOW_US = 1690; +static constexpr uint32_t BIT_ZERO_LOW_US = 560; +static constexpr uint32_t FOOTER_HIGH_US = 560; +static constexpr uint32_t FOOTER_LOW_US = 4500; +static constexpr uint16_t PACKET_SPACE = 5500; void ToshibaAcProtocol::encode(RemoteTransmitData *dst, const ToshibaAcData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/toto_protocol.cpp b/esphome/components/remote_base/toto_protocol.cpp index ba21263bc8..f08258c4a3 100644 --- a/esphome/components/remote_base/toto_protocol.cpp +++ b/esphome/components/remote_base/toto_protocol.cpp @@ -6,12 +6,12 @@ namespace remote_base { static const char *const TAG = "remote.toto"; -static const uint32_t PREAMBLE_HIGH_US = 6200; -static const uint32_t PREAMBLE_LOW_US = 2800; -static const uint32_t BIT_HIGH_US = 550; -static const uint32_t BIT_ONE_LOW_US = 1700; -static const uint32_t BIT_ZERO_LOW_US = 550; -static const uint32_t TOTO_HEADER = 0x2008; +static constexpr uint32_t PREAMBLE_HIGH_US = 6200; +static constexpr uint32_t PREAMBLE_LOW_US = 2800; +static constexpr uint32_t BIT_HIGH_US = 550; +static constexpr uint32_t BIT_ONE_LOW_US = 1700; +static constexpr uint32_t BIT_ZERO_LOW_US = 550; +static constexpr uint32_t TOTO_HEADER = 0x2008; void TotoProtocol::encode(RemoteTransmitData *dst, const TotoData &data) { uint32_t payload = 0; From dff9780d3af9f6ce41bf9b3db8e6138dc197c08f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:19:48 -0600 Subject: [PATCH 27/43] [core] Use constexpr for compile-time constants (#14071) --- esphome/core/application.h | 2 +- esphome/core/helpers.cpp | 6 +++--- esphome/core/scheduler.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index e0299f3db3..5b3e3dfed6 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -111,7 +111,7 @@ namespace esphome { // For reboots, it's more important to shut down quickly than disconnect cleanly // since we're not entering deep sleep. The only consequence of not shutting down // cleanly is a warning in the log. -static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick reboot +static constexpr uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick reboot class Application { public: diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index c2f7f67d9a..09e755ca71 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -846,9 +846,9 @@ void IRAM_ATTR HOT delay_microseconds_safe(uint32_t us) { // avoids CPU locks that could trigger WDT or affect WiFi/BT stability uint32_t start = micros(); - const uint32_t lag = 5000; // microseconds, specifies the maximum time for a CPU busy-loop. - // it must be larger than the worst-case duration of a delay(1) call (hardware tasks) - // 5ms is conservative, it could be reduced when exact BT/WiFi stack delays are known + constexpr uint32_t lag = 5000; // microseconds, specifies the maximum time for a CPU busy-loop. + // it must be larger than the worst-case duration of a delay(1) call (hardware tasks) + // 5ms is conservative, it could be reduced when exact BT/WiFi stack delays are known if (us > lag) { delay((us - lag) / 1000UL); // note: in disabled-interrupt contexts delay() won't actually sleep while (micros() - start < us - lag) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 4194c3aa9e..3294f689e8 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -728,7 +728,7 @@ uint64_t Scheduler::millis_64_(uint32_t now) { // Define a safe window around the rollover point (10 seconds) // This covers any reasonable scheduler delays or thread preemption - static const uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds + static constexpr uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds // Check if we're near the rollover boundary (close to std::numeric_limits::max() or just past 0) bool near_rollover = (last > (std::numeric_limits::max() - ROLLOVER_WINDOW)) || (now < ROLLOVER_WINDOW); From e7f202186450ad2f865ecbc865ca87b10b243dc3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:32:24 -0600 Subject: [PATCH 28/43] [http_request] Replace std::map with std::vector in action template (#14026) --- esphome/components/http_request/__init__.py | 6 +++++- esphome/components/http_request/http_request.h | 11 ++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 5faffccbe4..2d6ecae0bc 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -302,10 +302,14 @@ async def http_request_action_to_code(config, action_id, template_arg, args): lambda_ = await cg.process_lambda(json_, args_, return_type=cg.void) cg.add(var.set_json(lambda_)) else: + cg.add(var.init_json(len(json_))) for key in json_: template_ = await cg.templatable(json_[key], args, cg.std_string) cg.add(var.add_json(key, template_)) - for key, value in config.get(CONF_REQUEST_HEADERS, {}).items(): + request_headers = config.get(CONF_REQUEST_HEADERS, {}) + if request_headers: + cg.add(var.init_request_headers(len(request_headers))) + for key, value in request_headers.items(): template_ = await cg.templatable(value, args, cg.const_char_ptr) cg.add(var.add_request_header(key, template_)) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 458ffe94a8..2b2d05c63f 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include @@ -399,13 +398,15 @@ template class HttpRequestSendAction : public Action { TEMPLATABLE_VALUE(bool, capture_response) #endif + void init_request_headers(size_t count) { this->request_headers_.init(count); } void add_request_header(const char *key, TemplatableValue value) { - this->request_headers_.insert({key, value}); + this->request_headers_.push_back({key, value}); } void add_collect_header(const char *value) { this->lower_case_collect_headers_.push_back(value); } - void add_json(const char *key, TemplatableValue value) { this->json_.insert({key, value}); } + void init_json(size_t count) { this->json_.init(count); } + void add_json(const char *key, TemplatableValue value) { this->json_.push_back({key, value}); } void set_json(std::function json_func) { this->json_func_ = json_func; } @@ -507,9 +508,9 @@ template class HttpRequestSendAction : public Action { } void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); } HttpRequestComponent *parent_; - std::map> request_headers_{}; + FixedVector>> request_headers_{}; std::vector lower_case_collect_headers_{"content-type", "content-length"}; - std::map> json_{}; + FixedVector>> json_{}; std::function json_func_{nullptr}; #ifdef USE_HTTP_REQUEST_RESPONSE Trigger, std::string &, Ts...> success_trigger_with_response_; From eaf0d03a37a85ead5f33c5b3512d80944f2bfa09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:32:37 -0600 Subject: [PATCH 29/43] [ld2420] Use constexpr for compile-time constants (#14079) --- esphome/components/ld2420/ld2420.cpp | 118 +++++++++++++-------------- esphome/components/ld2420/ld2420.h | 6 +- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 69b69f4a61..cf78a1a460 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -63,73 +63,73 @@ namespace esphome::ld2420 { static const char *const TAG = "ld2420"; // Local const's -static const uint16_t REFRESH_RATE_MS = 1000; +static constexpr uint16_t REFRESH_RATE_MS = 1000; // Command sets -static const uint16_t CMD_DISABLE_CONF = 0x00FE; -static const uint16_t CMD_ENABLE_CONF = 0x00FF; -static const uint16_t CMD_PARM_HIGH_TRESH = 0x0012; -static const uint16_t CMD_PARM_LOW_TRESH = 0x0021; -static const uint16_t CMD_PROTOCOL_VER = 0x0002; -static const uint16_t CMD_READ_ABD_PARAM = 0x0008; -static const uint16_t CMD_READ_REG_ADDR = 0x0020; -static const uint16_t CMD_READ_REGISTER = 0x0002; -static const uint16_t CMD_READ_SERIAL_NUM = 0x0011; -static const uint16_t CMD_READ_SYS_PARAM = 0x0013; -static const uint16_t CMD_READ_VERSION = 0x0000; -static const uint16_t CMD_RESTART = 0x0068; -static const uint16_t CMD_SYSTEM_MODE = 0x0000; -static const uint16_t CMD_SYSTEM_MODE_GR = 0x0003; -static const uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; -static const uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; -static const uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; -static const uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; -static const uint16_t CMD_SYSTEM_MODE_VS = 0x0002; -static const uint16_t CMD_WRITE_ABD_PARAM = 0x0007; -static const uint16_t CMD_WRITE_REGISTER = 0x0001; -static const uint16_t CMD_WRITE_SYS_PARAM = 0x0012; +static constexpr uint16_t CMD_DISABLE_CONF = 0x00FE; +static constexpr uint16_t CMD_ENABLE_CONF = 0x00FF; +static constexpr uint16_t CMD_PARM_HIGH_TRESH = 0x0012; +static constexpr uint16_t CMD_PARM_LOW_TRESH = 0x0021; +static constexpr uint16_t CMD_PROTOCOL_VER = 0x0002; +static constexpr uint16_t CMD_READ_ABD_PARAM = 0x0008; +static constexpr uint16_t CMD_READ_REG_ADDR = 0x0020; +static constexpr uint16_t CMD_READ_REGISTER = 0x0002; +static constexpr uint16_t CMD_READ_SERIAL_NUM = 0x0011; +static constexpr uint16_t CMD_READ_SYS_PARAM = 0x0013; +static constexpr uint16_t CMD_READ_VERSION = 0x0000; +static constexpr uint16_t CMD_RESTART = 0x0068; +static constexpr uint16_t CMD_SYSTEM_MODE = 0x0000; +static constexpr uint16_t CMD_SYSTEM_MODE_GR = 0x0003; +static constexpr uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; +static constexpr uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; +static constexpr uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; +static constexpr uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; +static constexpr uint16_t CMD_SYSTEM_MODE_VS = 0x0002; +static constexpr uint16_t CMD_WRITE_ABD_PARAM = 0x0007; +static constexpr uint16_t CMD_WRITE_REGISTER = 0x0001; +static constexpr uint16_t CMD_WRITE_SYS_PARAM = 0x0012; -static const uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; -static const uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; -static const uint8_t CMD_MAX_BYTES = 0x64; -static const uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; +static constexpr uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; +static constexpr uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; +static constexpr uint8_t CMD_MAX_BYTES = 0x64; +static constexpr uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; -static const uint8_t LD2420_ERROR_NONE = 0x00; -static const uint8_t LD2420_ERROR_TIMEOUT = 0x02; -static const uint8_t LD2420_ERROR_UNKNOWN = 0x01; +static constexpr uint8_t LD2420_ERROR_NONE = 0x00; +static constexpr uint8_t LD2420_ERROR_TIMEOUT = 0x02; +static constexpr uint8_t LD2420_ERROR_UNKNOWN = 0x01; // Register address values -static const uint16_t CMD_MIN_GATE_REG = 0x0000; -static const uint16_t CMD_MAX_GATE_REG = 0x0001; -static const uint16_t CMD_TIMEOUT_REG = 0x0004; -static const uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, - 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, - 0x001C, 0x001D, 0x001E, 0x001F}; -static const uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, - 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, - 0x002C, 0x002D, 0x002E, 0x002F}; -static const uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, - 250, 250, 250, 250, 250, 250, 250, 250}; -static const uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, - 150, 100, 100, 100, 100, 100, 100, 100}; -static const uint16_t FACTORY_TIMEOUT = 120; -static const uint16_t FACTORY_MIN_GATE = 1; -static const uint16_t FACTORY_MAX_GATE = 12; +static constexpr uint16_t CMD_MIN_GATE_REG = 0x0000; +static constexpr uint16_t CMD_MAX_GATE_REG = 0x0001; +static constexpr uint16_t CMD_TIMEOUT_REG = 0x0004; +static constexpr uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, + 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, + 0x001C, 0x001D, 0x001E, 0x001F}; +static constexpr uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, + 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, + 0x002C, 0x002D, 0x002E, 0x002F}; +static constexpr uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, + 250, 250, 250, 250, 250, 250, 250, 250}; +static constexpr uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, + 150, 100, 100, 100, 100, 100, 100, 100}; +static constexpr uint16_t FACTORY_TIMEOUT = 120; +static constexpr uint16_t FACTORY_MIN_GATE = 1; +static constexpr uint16_t FACTORY_MAX_GATE = 12; // COMMAND_BYTE Header & Footer -static const uint32_t CMD_FRAME_FOOTER = 0x01020304; -static const uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; -static const uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; -static const uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; -static const uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; -static const uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; -static const int CALIBRATE_VERSION_MIN = 154; -static const uint8_t CMD_FRAME_COMMAND = 6; -static const uint8_t CMD_FRAME_DATA_LENGTH = 4; -static const uint8_t CMD_FRAME_STATUS = 7; -static const uint8_t CMD_ERROR_WORD = 8; -static const uint8_t ENERGY_SENSOR_START = 9; -static const uint8_t CALIBRATE_REPORT_INTERVAL = 4; +static constexpr uint32_t CMD_FRAME_FOOTER = 0x01020304; +static constexpr uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; +static constexpr uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; +static constexpr uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; +static constexpr uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; +static constexpr uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; +static constexpr int CALIBRATE_VERSION_MIN = 154; +static constexpr uint8_t CMD_FRAME_COMMAND = 6; +static constexpr uint8_t CMD_FRAME_DATA_LENGTH = 4; +static constexpr uint8_t CMD_FRAME_STATUS = 7; +static constexpr uint8_t CMD_ERROR_WORD = 8; +static constexpr uint8_t ENERGY_SENSOR_START = 9; +static constexpr uint8_t CALIBRATE_REPORT_INTERVAL = 4; static const char *const OP_NORMAL_MODE_STRING = "Normal"; static const char *const OP_SIMPLE_MODE_STRING = "Simple"; diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 6d81f86497..02250c5911 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -20,9 +20,9 @@ namespace esphome::ld2420 { -static const uint8_t CALIBRATE_SAMPLES = 64; -static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer -static const uint8_t TOTAL_GATES = 16; +static constexpr uint8_t CALIBRATE_SAMPLES = 64; +static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +static constexpr uint8_t TOTAL_GATES = 16; enum OpMode : uint8_t { OP_NORMAL_MODE = 1, From 9a8b00a42835b20db98081f1801c5cbafafc723a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:33:23 -0600 Subject: [PATCH 30/43] [nfc] Use constexpr for compile-time constants (#14077) --- esphome/components/nfc/nci_core.h | 228 +++++++++++------------ esphome/components/nfc/ndef_message.h | 2 +- esphome/components/nfc/ndef_record.h | 16 +- esphome/components/nfc/ndef_record_uri.h | 2 +- esphome/components/nfc/nfc.h | 60 +++--- 5 files changed, 154 insertions(+), 154 deletions(-) diff --git a/esphome/components/nfc/nci_core.h b/esphome/components/nfc/nci_core.h index fdaf6d0cc5..6b42070ed0 100644 --- a/esphome/components/nfc/nci_core.h +++ b/esphome/components/nfc/nci_core.h @@ -8,137 +8,137 @@ namespace esphome { namespace nfc { // Header info -static const uint8_t NCI_PKT_HEADER_SIZE = 3; // NCI packet (pkt) headers are always three bytes -static const uint8_t NCI_PKT_MT_GID_OFFSET = 0; // NCI packet (pkt) MT and GID offsets -static const uint8_t NCI_PKT_OID_OFFSET = 1; // NCI packet (pkt) OID offset -static const uint8_t NCI_PKT_LENGTH_OFFSET = 2; // NCI packet (pkt) message length (size) offset -static const uint8_t NCI_PKT_PAYLOAD_OFFSET = 3; // NCI packet (pkt) payload offset +static constexpr uint8_t NCI_PKT_HEADER_SIZE = 3; // NCI packet (pkt) headers are always three bytes +static constexpr uint8_t NCI_PKT_MT_GID_OFFSET = 0; // NCI packet (pkt) MT and GID offsets +static constexpr uint8_t NCI_PKT_OID_OFFSET = 1; // NCI packet (pkt) OID offset +static constexpr uint8_t NCI_PKT_LENGTH_OFFSET = 2; // NCI packet (pkt) message length (size) offset +static constexpr uint8_t NCI_PKT_PAYLOAD_OFFSET = 3; // NCI packet (pkt) payload offset // Important masks -static const uint8_t NCI_PKT_MT_MASK = 0xE0; // NCI packet (pkt) message type mask -static const uint8_t NCI_PKT_PBF_MASK = 0x10; // packet boundary flag bit -static const uint8_t NCI_PKT_GID_MASK = 0x0F; -static const uint8_t NCI_PKT_OID_MASK = 0x3F; +static constexpr uint8_t NCI_PKT_MT_MASK = 0xE0; // NCI packet (pkt) message type mask +static constexpr uint8_t NCI_PKT_PBF_MASK = 0x10; // packet boundary flag bit +static constexpr uint8_t NCI_PKT_GID_MASK = 0x0F; +static constexpr uint8_t NCI_PKT_OID_MASK = 0x3F; // Message types -static const uint8_t NCI_PKT_MT_DATA = 0x00; // For sending commands to NFC endpoint (card/tag) -static const uint8_t NCI_PKT_MT_CTRL_COMMAND = 0x20; // For sending commands to NFCC -static const uint8_t NCI_PKT_MT_CTRL_RESPONSE = 0x40; // Response from NFCC to commands -static const uint8_t NCI_PKT_MT_CTRL_NOTIFICATION = 0x60; // Notification from NFCC +static constexpr uint8_t NCI_PKT_MT_DATA = 0x00; // For sending commands to NFC endpoint (card/tag) +static constexpr uint8_t NCI_PKT_MT_CTRL_COMMAND = 0x20; // For sending commands to NFCC +static constexpr uint8_t NCI_PKT_MT_CTRL_RESPONSE = 0x40; // Response from NFCC to commands +static constexpr uint8_t NCI_PKT_MT_CTRL_NOTIFICATION = 0x60; // Notification from NFCC // GIDs -static const uint8_t NCI_CORE_GID = 0x0; -static const uint8_t RF_GID = 0x1; -static const uint8_t NFCEE_GID = 0x1; -static const uint8_t NCI_PROPRIETARY_GID = 0xF; +static constexpr uint8_t NCI_CORE_GID = 0x0; +static constexpr uint8_t RF_GID = 0x1; +static constexpr uint8_t NFCEE_GID = 0x1; +static constexpr uint8_t NCI_PROPRIETARY_GID = 0xF; // OIDs -static const uint8_t NCI_CORE_RESET_OID = 0x00; -static const uint8_t NCI_CORE_INIT_OID = 0x01; -static const uint8_t NCI_CORE_SET_CONFIG_OID = 0x02; -static const uint8_t NCI_CORE_GET_CONFIG_OID = 0x03; -static const uint8_t NCI_CORE_CONN_CREATE_OID = 0x04; -static const uint8_t NCI_CORE_CONN_CLOSE_OID = 0x05; -static const uint8_t NCI_CORE_CONN_CREDITS_OID = 0x06; -static const uint8_t NCI_CORE_GENERIC_ERROR_OID = 0x07; -static const uint8_t NCI_CORE_INTERFACE_ERROR_OID = 0x08; +static constexpr uint8_t NCI_CORE_RESET_OID = 0x00; +static constexpr uint8_t NCI_CORE_INIT_OID = 0x01; +static constexpr uint8_t NCI_CORE_SET_CONFIG_OID = 0x02; +static constexpr uint8_t NCI_CORE_GET_CONFIG_OID = 0x03; +static constexpr uint8_t NCI_CORE_CONN_CREATE_OID = 0x04; +static constexpr uint8_t NCI_CORE_CONN_CLOSE_OID = 0x05; +static constexpr uint8_t NCI_CORE_CONN_CREDITS_OID = 0x06; +static constexpr uint8_t NCI_CORE_GENERIC_ERROR_OID = 0x07; +static constexpr uint8_t NCI_CORE_INTERFACE_ERROR_OID = 0x08; -static const uint8_t RF_DISCOVER_MAP_OID = 0x00; -static const uint8_t RF_SET_LISTEN_MODE_ROUTING_OID = 0x01; -static const uint8_t RF_GET_LISTEN_MODE_ROUTING_OID = 0x02; -static const uint8_t RF_DISCOVER_OID = 0x03; -static const uint8_t RF_DISCOVER_SELECT_OID = 0x04; -static const uint8_t RF_INTF_ACTIVATED_OID = 0x05; -static const uint8_t RF_DEACTIVATE_OID = 0x06; -static const uint8_t RF_FIELD_INFO_OID = 0x07; -static const uint8_t RF_T3T_POLLING_OID = 0x08; -static const uint8_t RF_NFCEE_ACTION_OID = 0x09; -static const uint8_t RF_NFCEE_DISCOVERY_REQ_OID = 0x0A; -static const uint8_t RF_PARAMETER_UPDATE_OID = 0x0B; +static constexpr uint8_t RF_DISCOVER_MAP_OID = 0x00; +static constexpr uint8_t RF_SET_LISTEN_MODE_ROUTING_OID = 0x01; +static constexpr uint8_t RF_GET_LISTEN_MODE_ROUTING_OID = 0x02; +static constexpr uint8_t RF_DISCOVER_OID = 0x03; +static constexpr uint8_t RF_DISCOVER_SELECT_OID = 0x04; +static constexpr uint8_t RF_INTF_ACTIVATED_OID = 0x05; +static constexpr uint8_t RF_DEACTIVATE_OID = 0x06; +static constexpr uint8_t RF_FIELD_INFO_OID = 0x07; +static constexpr uint8_t RF_T3T_POLLING_OID = 0x08; +static constexpr uint8_t RF_NFCEE_ACTION_OID = 0x09; +static constexpr uint8_t RF_NFCEE_DISCOVERY_REQ_OID = 0x0A; +static constexpr uint8_t RF_PARAMETER_UPDATE_OID = 0x0B; -static const uint8_t NFCEE_DISCOVER_OID = 0x00; -static const uint8_t NFCEE_MODE_SET_OID = 0x01; +static constexpr uint8_t NFCEE_DISCOVER_OID = 0x00; +static constexpr uint8_t NFCEE_MODE_SET_OID = 0x01; // Interfaces -static const uint8_t INTF_NFCEE_DIRECT = 0x00; -static const uint8_t INTF_FRAME = 0x01; -static const uint8_t INTF_ISODEP = 0x02; -static const uint8_t INTF_NFCDEP = 0x03; -static const uint8_t INTF_TAGCMD = 0x80; // NXP proprietary +static constexpr uint8_t INTF_NFCEE_DIRECT = 0x00; +static constexpr uint8_t INTF_FRAME = 0x01; +static constexpr uint8_t INTF_ISODEP = 0x02; +static constexpr uint8_t INTF_NFCDEP = 0x03; +static constexpr uint8_t INTF_TAGCMD = 0x80; // NXP proprietary // Bit rates -static const uint8_t NFC_BIT_RATE_106 = 0x00; -static const uint8_t NFC_BIT_RATE_212 = 0x01; -static const uint8_t NFC_BIT_RATE_424 = 0x02; -static const uint8_t NFC_BIT_RATE_848 = 0x03; -static const uint8_t NFC_BIT_RATE_1695 = 0x04; -static const uint8_t NFC_BIT_RATE_3390 = 0x05; -static const uint8_t NFC_BIT_RATE_6780 = 0x06; +static constexpr uint8_t NFC_BIT_RATE_106 = 0x00; +static constexpr uint8_t NFC_BIT_RATE_212 = 0x01; +static constexpr uint8_t NFC_BIT_RATE_424 = 0x02; +static constexpr uint8_t NFC_BIT_RATE_848 = 0x03; +static constexpr uint8_t NFC_BIT_RATE_1695 = 0x04; +static constexpr uint8_t NFC_BIT_RATE_3390 = 0x05; +static constexpr uint8_t NFC_BIT_RATE_6780 = 0x06; // Protocols -static const uint8_t PROT_UNDETERMINED = 0x00; -static const uint8_t PROT_T1T = 0x01; -static const uint8_t PROT_T2T = 0x02; -static const uint8_t PROT_T3T = 0x03; -static const uint8_t PROT_ISODEP = 0x04; -static const uint8_t PROT_NFCDEP = 0x05; -static const uint8_t PROT_T5T = 0x06; -static const uint8_t PROT_MIFARE = 0x80; +static constexpr uint8_t PROT_UNDETERMINED = 0x00; +static constexpr uint8_t PROT_T1T = 0x01; +static constexpr uint8_t PROT_T2T = 0x02; +static constexpr uint8_t PROT_T3T = 0x03; +static constexpr uint8_t PROT_ISODEP = 0x04; +static constexpr uint8_t PROT_NFCDEP = 0x05; +static constexpr uint8_t PROT_T5T = 0x06; +static constexpr uint8_t PROT_MIFARE = 0x80; // RF Technologies -static const uint8_t NFC_RF_TECH_A = 0x00; -static const uint8_t NFC_RF_TECH_B = 0x01; -static const uint8_t NFC_RF_TECH_F = 0x02; -static const uint8_t NFC_RF_TECH_15693 = 0x03; +static constexpr uint8_t NFC_RF_TECH_A = 0x00; +static constexpr uint8_t NFC_RF_TECH_B = 0x01; +static constexpr uint8_t NFC_RF_TECH_F = 0x02; +static constexpr uint8_t NFC_RF_TECH_15693 = 0x03; // RF Technology & Modes -static const uint8_t MODE_MASK = 0xF0; -static const uint8_t MODE_LISTEN_MASK = 0x80; -static const uint8_t MODE_POLL = 0x00; +static constexpr uint8_t MODE_MASK = 0xF0; +static constexpr uint8_t MODE_LISTEN_MASK = 0x80; +static constexpr uint8_t MODE_POLL = 0x00; -static const uint8_t TECH_PASSIVE_NFCA = 0x00; -static const uint8_t TECH_PASSIVE_NFCB = 0x01; -static const uint8_t TECH_PASSIVE_NFCF = 0x02; -static const uint8_t TECH_ACTIVE_NFCA = 0x03; -static const uint8_t TECH_ACTIVE_NFCF = 0x05; -static const uint8_t TECH_PASSIVE_15693 = 0x06; +static constexpr uint8_t TECH_PASSIVE_NFCA = 0x00; +static constexpr uint8_t TECH_PASSIVE_NFCB = 0x01; +static constexpr uint8_t TECH_PASSIVE_NFCF = 0x02; +static constexpr uint8_t TECH_ACTIVE_NFCA = 0x03; +static constexpr uint8_t TECH_ACTIVE_NFCF = 0x05; +static constexpr uint8_t TECH_PASSIVE_15693 = 0x06; // Status codes -static const uint8_t STATUS_OK = 0x00; -static const uint8_t STATUS_REJECTED = 0x01; -static const uint8_t STATUS_RF_FRAME_CORRUPTED = 0x02; -static const uint8_t STATUS_FAILED = 0x03; -static const uint8_t STATUS_NOT_INITIALIZED = 0x04; -static const uint8_t STATUS_SYNTAX_ERROR = 0x05; -static const uint8_t STATUS_SEMANTIC_ERROR = 0x06; -static const uint8_t STATUS_INVALID_PARAM = 0x09; -static const uint8_t STATUS_MESSAGE_SIZE_EXCEEDED = 0x0A; -static const uint8_t DISCOVERY_ALREADY_STARTED = 0xA0; -static const uint8_t DISCOVERY_TARGET_ACTIVATION_FAILED = 0xA1; -static const uint8_t DISCOVERY_TEAR_DOWN = 0xA2; -static const uint8_t RF_TRANSMISSION_ERROR = 0xB0; -static const uint8_t RF_PROTOCOL_ERROR = 0xB1; -static const uint8_t RF_TIMEOUT_ERROR = 0xB2; -static const uint8_t NFCEE_INTERFACE_ACTIVATION_FAILED = 0xC0; -static const uint8_t NFCEE_TRANSMISSION_ERROR = 0xC1; -static const uint8_t NFCEE_PROTOCOL_ERROR = 0xC2; -static const uint8_t NFCEE_TIMEOUT_ERROR = 0xC3; +static constexpr uint8_t STATUS_OK = 0x00; +static constexpr uint8_t STATUS_REJECTED = 0x01; +static constexpr uint8_t STATUS_RF_FRAME_CORRUPTED = 0x02; +static constexpr uint8_t STATUS_FAILED = 0x03; +static constexpr uint8_t STATUS_NOT_INITIALIZED = 0x04; +static constexpr uint8_t STATUS_SYNTAX_ERROR = 0x05; +static constexpr uint8_t STATUS_SEMANTIC_ERROR = 0x06; +static constexpr uint8_t STATUS_INVALID_PARAM = 0x09; +static constexpr uint8_t STATUS_MESSAGE_SIZE_EXCEEDED = 0x0A; +static constexpr uint8_t DISCOVERY_ALREADY_STARTED = 0xA0; +static constexpr uint8_t DISCOVERY_TARGET_ACTIVATION_FAILED = 0xA1; +static constexpr uint8_t DISCOVERY_TEAR_DOWN = 0xA2; +static constexpr uint8_t RF_TRANSMISSION_ERROR = 0xB0; +static constexpr uint8_t RF_PROTOCOL_ERROR = 0xB1; +static constexpr uint8_t RF_TIMEOUT_ERROR = 0xB2; +static constexpr uint8_t NFCEE_INTERFACE_ACTIVATION_FAILED = 0xC0; +static constexpr uint8_t NFCEE_TRANSMISSION_ERROR = 0xC1; +static constexpr uint8_t NFCEE_PROTOCOL_ERROR = 0xC2; +static constexpr uint8_t NFCEE_TIMEOUT_ERROR = 0xC3; // Deactivation types/reasons -static const uint8_t DEACTIVATION_TYPE_IDLE = 0x00; -static const uint8_t DEACTIVATION_TYPE_SLEEP = 0x01; -static const uint8_t DEACTIVATION_TYPE_SLEEP_AF = 0x02; -static const uint8_t DEACTIVATION_TYPE_DISCOVERY = 0x03; +static constexpr uint8_t DEACTIVATION_TYPE_IDLE = 0x00; +static constexpr uint8_t DEACTIVATION_TYPE_SLEEP = 0x01; +static constexpr uint8_t DEACTIVATION_TYPE_SLEEP_AF = 0x02; +static constexpr uint8_t DEACTIVATION_TYPE_DISCOVERY = 0x03; // RF discover map modes -static const uint8_t RF_DISCOVER_MAP_MODE_POLL = 0x1; -static const uint8_t RF_DISCOVER_MAP_MODE_LISTEN = 0x2; +static constexpr uint8_t RF_DISCOVER_MAP_MODE_POLL = 0x1; +static constexpr uint8_t RF_DISCOVER_MAP_MODE_LISTEN = 0x2; // RF discover notification types -static const uint8_t RF_DISCOVER_NTF_NT_LAST = 0x00; -static const uint8_t RF_DISCOVER_NTF_NT_LAST_RL = 0x01; -static const uint8_t RF_DISCOVER_NTF_NT_MORE = 0x02; +static constexpr uint8_t RF_DISCOVER_NTF_NT_LAST = 0x00; +static constexpr uint8_t RF_DISCOVER_NTF_NT_LAST_RL = 0x01; +static constexpr uint8_t RF_DISCOVER_NTF_NT_MORE = 0x02; // Important message offsets -static const uint8_t RF_DISCOVER_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_DISCOVER_NTF_PROTOCOL = 1 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_DISCOVER_NTF_MODE_TECH = 2 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_DISCOVER_NTF_RF_TECH_LENGTH = 3 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_DISCOVER_NTF_RF_TECH_PARAMS = 4 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_INTERFACE = 1 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_PROTOCOL = 2 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_MODE_TECH = 3 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_MAX_SIZE = 4 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_INIT_CRED = 5 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_LENGTH = 6 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_PARAMS = 7 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_PROTOCOL = 1 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_MODE_TECH = 2 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_RF_TECH_LENGTH = 3 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_RF_TECH_PARAMS = 4 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_INTERFACE = 1 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_PROTOCOL = 2 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_MODE_TECH = 3 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_MAX_SIZE = 4 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_INIT_CRED = 5 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_LENGTH = 6 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_PARAMS = 7 + NCI_PKT_HEADER_SIZE; } // namespace nfc } // namespace esphome diff --git a/esphome/components/nfc/ndef_message.h b/esphome/components/nfc/ndef_message.h index 2e81fb216c..48f79b8854 100644 --- a/esphome/components/nfc/ndef_message.h +++ b/esphome/components/nfc/ndef_message.h @@ -12,7 +12,7 @@ namespace esphome { namespace nfc { -static const uint8_t MAX_NDEF_RECORDS = 4; +static constexpr uint8_t MAX_NDEF_RECORDS = 4; class NdefMessage { public: diff --git a/esphome/components/nfc/ndef_record.h b/esphome/components/nfc/ndef_record.h index 76d8b6a50a..1a7c24aee9 100644 --- a/esphome/components/nfc/ndef_record.h +++ b/esphome/components/nfc/ndef_record.h @@ -8,14 +8,14 @@ namespace esphome { namespace nfc { -static const uint8_t TNF_EMPTY = 0x00; -static const uint8_t TNF_WELL_KNOWN = 0x01; -static const uint8_t TNF_MIME_MEDIA = 0x02; -static const uint8_t TNF_ABSOLUTE_URI = 0x03; -static const uint8_t TNF_EXTERNAL_TYPE = 0x04; -static const uint8_t TNF_UNKNOWN = 0x05; -static const uint8_t TNF_UNCHANGED = 0x06; -static const uint8_t TNF_RESERVED = 0x07; +static constexpr uint8_t TNF_EMPTY = 0x00; +static constexpr uint8_t TNF_WELL_KNOWN = 0x01; +static constexpr uint8_t TNF_MIME_MEDIA = 0x02; +static constexpr uint8_t TNF_ABSOLUTE_URI = 0x03; +static constexpr uint8_t TNF_EXTERNAL_TYPE = 0x04; +static constexpr uint8_t TNF_UNKNOWN = 0x05; +static constexpr uint8_t TNF_UNCHANGED = 0x06; +static constexpr uint8_t TNF_RESERVED = 0x07; class NdefRecord { public: diff --git a/esphome/components/nfc/ndef_record_uri.h b/esphome/components/nfc/ndef_record_uri.h index 1eadda1b4f..2f7790a9a9 100644 --- a/esphome/components/nfc/ndef_record_uri.h +++ b/esphome/components/nfc/ndef_record_uri.h @@ -9,7 +9,7 @@ namespace esphome { namespace nfc { -static const uint8_t PAYLOAD_IDENTIFIERS_COUNT = 0x23; +static constexpr uint8_t PAYLOAD_IDENTIFIERS_COUNT = 0x23; static const char *const PAYLOAD_IDENTIFIERS[] = {"", "http://www.", "https://www.", diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h index 5191904833..cdaea82af6 100644 --- a/esphome/components/nfc/nfc.h +++ b/esphome/components/nfc/nfc.h @@ -12,47 +12,47 @@ namespace esphome { namespace nfc { -static const uint8_t MIFARE_CLASSIC_BLOCK_SIZE = 16; -static const uint8_t MIFARE_CLASSIC_LONG_TLV_SIZE = 4; -static const uint8_t MIFARE_CLASSIC_SHORT_TLV_SIZE = 2; -static const uint8_t MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW = 4; -static const uint8_t MIFARE_CLASSIC_BLOCKS_PER_SECT_HIGH = 16; -static const uint8_t MIFARE_CLASSIC_16BLOCK_SECT_START = 32; +static constexpr uint8_t MIFARE_CLASSIC_BLOCK_SIZE = 16; +static constexpr uint8_t MIFARE_CLASSIC_LONG_TLV_SIZE = 4; +static constexpr uint8_t MIFARE_CLASSIC_SHORT_TLV_SIZE = 2; +static constexpr uint8_t MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW = 4; +static constexpr uint8_t MIFARE_CLASSIC_BLOCKS_PER_SECT_HIGH = 16; +static constexpr uint8_t MIFARE_CLASSIC_16BLOCK_SECT_START = 32; -static const uint8_t MIFARE_ULTRALIGHT_PAGE_SIZE = 4; -static const uint8_t MIFARE_ULTRALIGHT_READ_SIZE = 4; -static const uint8_t MIFARE_ULTRALIGHT_DATA_START_PAGE = 4; -static const uint8_t MIFARE_ULTRALIGHT_MAX_PAGE = 63; +static constexpr uint8_t MIFARE_ULTRALIGHT_PAGE_SIZE = 4; +static constexpr uint8_t MIFARE_ULTRALIGHT_READ_SIZE = 4; +static constexpr uint8_t MIFARE_ULTRALIGHT_DATA_START_PAGE = 4; +static constexpr uint8_t MIFARE_ULTRALIGHT_MAX_PAGE = 63; -static const uint8_t TAG_TYPE_MIFARE_CLASSIC = 0; -static const uint8_t TAG_TYPE_1 = 1; -static const uint8_t TAG_TYPE_2 = 2; -static const uint8_t TAG_TYPE_3 = 3; -static const uint8_t TAG_TYPE_4 = 4; -static const uint8_t TAG_TYPE_UNKNOWN = 99; +static constexpr uint8_t TAG_TYPE_MIFARE_CLASSIC = 0; +static constexpr uint8_t TAG_TYPE_1 = 1; +static constexpr uint8_t TAG_TYPE_2 = 2; +static constexpr uint8_t TAG_TYPE_3 = 3; +static constexpr uint8_t TAG_TYPE_4 = 4; +static constexpr uint8_t TAG_TYPE_UNKNOWN = 99; // Mifare Commands -static const uint8_t MIFARE_CMD_AUTH_A = 0x60; -static const uint8_t MIFARE_CMD_AUTH_B = 0x61; -static const uint8_t MIFARE_CMD_HALT = 0x50; -static const uint8_t MIFARE_CMD_READ = 0x30; -static const uint8_t MIFARE_CMD_WRITE = 0xA0; -static const uint8_t MIFARE_CMD_WRITE_ULTRALIGHT = 0xA2; +static constexpr uint8_t MIFARE_CMD_AUTH_A = 0x60; +static constexpr uint8_t MIFARE_CMD_AUTH_B = 0x61; +static constexpr uint8_t MIFARE_CMD_HALT = 0x50; +static constexpr uint8_t MIFARE_CMD_READ = 0x30; +static constexpr uint8_t MIFARE_CMD_WRITE = 0xA0; +static constexpr uint8_t MIFARE_CMD_WRITE_ULTRALIGHT = 0xA2; // Mifare Ack/Nak -static const uint8_t MIFARE_CMD_ACK = 0x0A; -static const uint8_t MIFARE_CMD_NAK_INVALID_XFER_BUFF_VALID = 0x00; -static const uint8_t MIFARE_CMD_NAK_CRC_ERROR_XFER_BUFF_VALID = 0x01; -static const uint8_t MIFARE_CMD_NAK_INVALID_XFER_BUFF_INVALID = 0x04; -static const uint8_t MIFARE_CMD_NAK_CRC_ERROR_XFER_BUFF_INVALID = 0x05; +static constexpr uint8_t MIFARE_CMD_ACK = 0x0A; +static constexpr uint8_t MIFARE_CMD_NAK_INVALID_XFER_BUFF_VALID = 0x00; +static constexpr uint8_t MIFARE_CMD_NAK_CRC_ERROR_XFER_BUFF_VALID = 0x01; +static constexpr uint8_t MIFARE_CMD_NAK_INVALID_XFER_BUFF_INVALID = 0x04; +static constexpr uint8_t MIFARE_CMD_NAK_CRC_ERROR_XFER_BUFF_INVALID = 0x05; static const char *const MIFARE_CLASSIC = "Mifare Classic"; static const char *const NFC_FORUM_TYPE_2 = "NFC Forum Type 2"; static const char *const ERROR = "Error"; -static const uint8_t DEFAULT_KEY[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; -static const uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7}; -static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}; +static constexpr uint8_t DEFAULT_KEY[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static constexpr uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7}; +static constexpr uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}; /// Max UID size is 10 bytes, formatted as "XX-XX-XX-XX-XX-XX-XX-XX-XX-XX\0" = 30 chars static constexpr size_t FORMAT_UID_BUFFER_SIZE = 30; From 2f9b76f129c2b278b0c45ba5c66430d638631ee2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:33:39 -0600 Subject: [PATCH 31/43] [pn7160] Use constexpr for compile-time constants (#14078) --- esphome/components/pn7150/pn7150.h | 111 +++++++++---------- esphome/components/pn7160/pn7160.h | 121 +++++++++++---------- esphome/components/pn7160_spi/pn7160_spi.h | 4 +- 3 files changed, 119 insertions(+), 117 deletions(-) diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h index 5feba17d21..c5dd283832 100644 --- a/esphome/components/pn7150/pn7150.h +++ b/esphome/components/pn7150/pn7150.h @@ -14,48 +14,48 @@ namespace esphome { namespace pn7150 { -static const uint16_t NFCC_DEFAULT_TIMEOUT = 10; -static const uint16_t NFCC_INIT_TIMEOUT = 50; -static const uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; +static constexpr uint16_t NFCC_DEFAULT_TIMEOUT = 10; +static constexpr uint16_t NFCC_INIT_TIMEOUT = 50; +static constexpr uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; -static const uint8_t NFCC_MAX_COMM_FAILS = 3; -static const uint8_t NFCC_MAX_ERROR_COUNT = 10; +static constexpr uint8_t NFCC_MAX_COMM_FAILS = 3; +static constexpr uint8_t NFCC_MAX_ERROR_COUNT = 10; -static const uint8_t XCHG_DATA_OID = 0x10; -static const uint8_t MF_SECTORSEL_OID = 0x32; -static const uint8_t MFC_AUTHENTICATE_OID = 0x40; -static const uint8_t TEST_PRBS_OID = 0x30; -static const uint8_t TEST_ANTENNA_OID = 0x3D; -static const uint8_t TEST_GET_REGISTER_OID = 0x33; +static constexpr uint8_t XCHG_DATA_OID = 0x10; +static constexpr uint8_t MF_SECTORSEL_OID = 0x32; +static constexpr uint8_t MFC_AUTHENTICATE_OID = 0x40; +static constexpr uint8_t TEST_PRBS_OID = 0x30; +static constexpr uint8_t TEST_ANTENNA_OID = 0x3D; +static constexpr uint8_t TEST_GET_REGISTER_OID = 0x33; -static const uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A -static const uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B -static const uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; -static const uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, - 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; -static const uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, - 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; -static const uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; -static const uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; -static const uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; -static const uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; -static const uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; -static const uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; +static constexpr uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, + 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, + 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; +static constexpr uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; +static constexpr uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; +static constexpr uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; +static constexpr uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; -static const uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields - 0x00, // config param identifier (TOTAL_DURATION) - 0x02, // length of value - 0x01, // TOTAL_DURATION (low)... - 0x00}; // TOTAL_DURATION (high): 1 ms +static constexpr uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0x01, // TOTAL_DURATION (low)... + 0x00}; // TOTAL_DURATION (high): 1 ms -static const uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields - 0x00, // config param identifier (TOTAL_DURATION) - 0x02, // length of value - 0xF8, // TOTAL_DURATION (low)... - 0x02}; // TOTAL_DURATION (high): 760 ms +static constexpr uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0xF8, // TOTAL_DURATION (low)... + 0x02}; // TOTAL_DURATION (high): 760 ms -static const uint8_t PMU_CFG[] = { +static constexpr uint8_t PMU_CFG[] = { 0x01, // Number of parameters 0xA0, 0x0E, // ext. tag 3, // length @@ -64,7 +64,7 @@ static const uint8_t PMU_CFG[] = { 0x01, // RFU; must be 0x00 for CFG1 and 0x01 for CFG2 }; -static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes +static constexpr uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL, nfc::INTF_FRAME, // poll mode nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL, @@ -76,28 +76,29 @@ static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL, nfc::INTF_TAGCMD}; // poll mode -static const uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode +static constexpr uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = { + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode -static const uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode +static constexpr uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode -static const uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode +static constexpr uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode -static const uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) - 1, // number of table entries - 0x01, // type = protocol-based - 3, // length - 0, // DH NFCEE ID, a static ID representing the DH-NFCEE - 0x01, // power state - nfc::PROT_ISODEP}; // protocol +static constexpr uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) + 1, // number of table entries + 0x01, // type = protocol-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x01, // power state + nfc::PROT_ISODEP}; // protocol enum class CardEmulationState : uint8_t { CARD_EMU_IDLE, diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h index 9f2d10c2d5..77ab49399c 100644 --- a/esphome/components/pn7160/pn7160.h +++ b/esphome/components/pn7160/pn7160.h @@ -14,48 +14,48 @@ namespace esphome { namespace pn7160 { -static const uint16_t NFCC_DEFAULT_TIMEOUT = 10; -static const uint16_t NFCC_INIT_TIMEOUT = 50; -static const uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; +static constexpr uint16_t NFCC_DEFAULT_TIMEOUT = 10; +static constexpr uint16_t NFCC_INIT_TIMEOUT = 50; +static constexpr uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; -static const uint8_t NFCC_MAX_COMM_FAILS = 3; -static const uint8_t NFCC_MAX_ERROR_COUNT = 10; +static constexpr uint8_t NFCC_MAX_COMM_FAILS = 3; +static constexpr uint8_t NFCC_MAX_ERROR_COUNT = 10; -static const uint8_t XCHG_DATA_OID = 0x10; -static const uint8_t MF_SECTORSEL_OID = 0x32; -static const uint8_t MFC_AUTHENTICATE_OID = 0x40; -static const uint8_t TEST_PRBS_OID = 0x30; -static const uint8_t TEST_ANTENNA_OID = 0x3D; -static const uint8_t TEST_GET_REGISTER_OID = 0x33; +static constexpr uint8_t XCHG_DATA_OID = 0x10; +static constexpr uint8_t MF_SECTORSEL_OID = 0x32; +static constexpr uint8_t MFC_AUTHENTICATE_OID = 0x40; +static constexpr uint8_t TEST_PRBS_OID = 0x30; +static constexpr uint8_t TEST_ANTENNA_OID = 0x3D; +static constexpr uint8_t TEST_GET_REGISTER_OID = 0x33; -static const uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A -static const uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B -static const uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; -static const uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, - 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; -static const uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, - 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; -static const uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; -static const uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; -static const uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; -static const uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; -static const uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; -static const uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; +static constexpr uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, + 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, + 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; +static constexpr uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; +static constexpr uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; +static constexpr uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; +static constexpr uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; -static const uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields - 0x00, // config param identifier (TOTAL_DURATION) - 0x02, // length of value - 0x01, // TOTAL_DURATION (low)... - 0x00}; // TOTAL_DURATION (high): 1 ms +static constexpr uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0x01, // TOTAL_DURATION (low)... + 0x00}; // TOTAL_DURATION (high): 1 ms -static const uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields - 0x00, // config param identifier (TOTAL_DURATION) - 0x02, // length of value - 0xF8, // TOTAL_DURATION (low)... - 0x02}; // TOTAL_DURATION (high): 760 ms +static constexpr uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0xF8, // TOTAL_DURATION (low)... + 0x02}; // TOTAL_DURATION (high): 760 ms -static const uint8_t PMU_CFG[] = { +static constexpr uint8_t PMU_CFG[] = { 0x01, // Number of parameters 0xA0, 0x0E, // ext. tag 11, // length @@ -74,7 +74,7 @@ static const uint8_t PMU_CFG[] = { 0x0C, // RFU }; -static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes +static constexpr uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL, nfc::INTF_FRAME, // poll mode nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL, @@ -86,33 +86,34 @@ static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL, nfc::INTF_TAGCMD}; // poll mode -static const uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode +static constexpr uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = { + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode -static const uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode +static constexpr uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode -static const uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode +static constexpr uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode -static const uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) - 2, // number of table entries - 0x01, // type = protocol-based - 3, // length - 0, // DH NFCEE ID, a static ID representing the DH-NFCEE - 0x07, // power state - nfc::PROT_ISODEP, // protocol - 0x00, // type = technology-based - 3, // length - 0, // DH NFCEE ID, a static ID representing the DH-NFCEE - 0x07, // power state - nfc::TECH_PASSIVE_NFCA}; // technology +static constexpr uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) + 2, // number of table entries + 0x01, // type = protocol-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x07, // power state + nfc::PROT_ISODEP, // protocol + 0x00, // type = technology-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x07, // power state + nfc::TECH_PASSIVE_NFCA}; // technology enum class CardEmulationState : uint8_t { CARD_EMU_IDLE, diff --git a/esphome/components/pn7160_spi/pn7160_spi.h b/esphome/components/pn7160_spi/pn7160_spi.h index 7d4460a76d..9b6e21fa2a 100644 --- a/esphome/components/pn7160_spi/pn7160_spi.h +++ b/esphome/components/pn7160_spi/pn7160_spi.h @@ -10,8 +10,8 @@ namespace esphome { namespace pn7160_spi { -static const uint8_t TDD_SPI_READ = 0xFF; -static const uint8_t TDD_SPI_WRITE = 0x0A; +static constexpr uint8_t TDD_SPI_READ = 0xFF; +static constexpr uint8_t TDD_SPI_WRITE = 0x0A; class PN7160Spi : public pn7160::PN7160, public spi::SPIDevice Date: Wed, 18 Feb 2026 21:34:25 -0600 Subject: [PATCH 32/43] [bluetooth_proxy] Use constexpr for remaining compile-time constants (#14080) --- esphome/components/bluetooth_proxy/bluetooth_proxy.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 62a035e79f..85461755aa 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -23,9 +23,9 @@ namespace esphome::bluetooth_proxy { -static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; -static const int DONE_SENDING_SERVICES = -2; -static const int INIT_SENDING_SERVICES = -3; +static constexpr esp_err_t ESP_GATT_NOT_CONNECTED = -1; +static constexpr int DONE_SENDING_SERVICES = -2; +static constexpr int INIT_SENDING_SERVICES = -3; using namespace esp32_ble_client; From 3c227eeca46d5ca6e99e5e59a9585d0f168642fb Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 18 Feb 2026 21:50:39 -0600 Subject: [PATCH 33/43] [audio] Add support for sinking via an arbitrary callback (#14035) --- esphome/components/audio/audio_transfer_buffer.cpp | 2 ++ esphome/components/audio/audio_transfer_buffer.h | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/esphome/components/audio/audio_transfer_buffer.cpp b/esphome/components/audio/audio_transfer_buffer.cpp index ddb669e0eb..a8be55d62f 100644 --- a/esphome/components/audio/audio_transfer_buffer.cpp +++ b/esphome/components/audio/audio_transfer_buffer.cpp @@ -165,6 +165,8 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, if (this->ring_buffer_.use_count() > 0) { bytes_written = this->ring_buffer_->write_without_replacement((void *) this->data_start_, this->available(), ticks_to_wait); + } else if (this->sink_callback_ != nullptr) { + bytes_written = this->sink_callback_->audio_sink_write(this->data_start_, this->available(), ticks_to_wait); } this->decrease_buffer_length(bytes_written); diff --git a/esphome/components/audio/audio_transfer_buffer.h b/esphome/components/audio/audio_transfer_buffer.h index 24c0670d1a..22c22cc9ae 100644 --- a/esphome/components/audio/audio_transfer_buffer.h +++ b/esphome/components/audio/audio_transfer_buffer.h @@ -15,6 +15,12 @@ namespace esphome { namespace audio { +/// @brief Abstract interface for writing decoded audio data to a sink. +class AudioSinkCallback { + public: + virtual size_t audio_sink_write(uint8_t *data, size_t length, TickType_t ticks_to_wait) = 0; +}; + class AudioTransferBuffer { /* * @brief Class that facilitates tranferring data between a buffer and an audio source or sink. @@ -108,6 +114,10 @@ class AudioSinkTransferBuffer : public AudioTransferBuffer { void set_sink(speaker::Speaker *speaker) { this->speaker_ = speaker; } #endif + /// @brief Adds a callback as the transfer buffer's sink. + /// @param callback Pointer to the AudioSinkCallback implementation + void set_sink(AudioSinkCallback *callback) { this->sink_callback_ = callback; } + void clear_buffered_data() override; bool has_buffered_data() const override; @@ -116,6 +126,7 @@ class AudioSinkTransferBuffer : public AudioTransferBuffer { #ifdef USE_SPEAKER speaker::Speaker *speaker_{nullptr}; #endif + AudioSinkCallback *sink_callback_{nullptr}; }; class AudioSourceTransferBuffer : public AudioTransferBuffer { From 264c8faedd0ab956b0437ec2ecc0bacae1708923 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 18 Feb 2026 21:51:01 -0600 Subject: [PATCH 34/43] [media_player] Add more commands to support Sendspin (#12258) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/media_player/__init__.py | 250 +++++++----------- esphome/components/media_player/automation.h | 27 ++ .../components/media_player/media_player.cpp | 52 ++++ .../components/media_player/media_player.h | 46 ++-- tests/components/media_player/common.yaml | 25 ++ 5 files changed, 232 insertions(+), 168 deletions(-) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index c6ffe50d79..b2afbe5e58 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -35,86 +35,73 @@ MEDIA_PLAYER_FORMAT_PURPOSE_ENUM = { "announcement": MediaPlayerFormatPurpose.PURPOSE_ANNOUNCEMENT, } - -PlayAction = media_player_ns.class_( - "PlayAction", automation.Action, cg.Parented.template(MediaPlayer) -) -PlayMediaAction = media_player_ns.class_( - "PlayMediaAction", automation.Action, cg.Parented.template(MediaPlayer) -) -ToggleAction = media_player_ns.class_( - "ToggleAction", automation.Action, cg.Parented.template(MediaPlayer) -) -PauseAction = media_player_ns.class_( - "PauseAction", automation.Action, cg.Parented.template(MediaPlayer) -) -StopAction = media_player_ns.class_( - "StopAction", automation.Action, cg.Parented.template(MediaPlayer) -) -VolumeUpAction = media_player_ns.class_( - "VolumeUpAction", automation.Action, cg.Parented.template(MediaPlayer) -) -VolumeDownAction = media_player_ns.class_( - "VolumeDownAction", automation.Action, cg.Parented.template(MediaPlayer) -) -VolumeSetAction = media_player_ns.class_( - "VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer) -) -TurnOnAction = media_player_ns.class_( - "TurnOnAction", automation.Action, cg.Parented.template(MediaPlayer) -) -TurnOffAction = media_player_ns.class_( - "TurnOffAction", automation.Action, cg.Parented.template(MediaPlayer) -) - +# Local config key constants CONF_ANNOUNCEMENT = "announcement" CONF_ON_PLAY = "on_play" CONF_ON_PAUSE = "on_pause" CONF_ON_ANNOUNCEMENT = "on_announcement" CONF_MEDIA_URL = "media_url" -StateTrigger = media_player_ns.class_("StateTrigger", automation.Trigger.template()) -IdleTrigger = media_player_ns.class_("IdleTrigger", automation.Trigger.template()) -PlayTrigger = media_player_ns.class_("PlayTrigger", automation.Trigger.template()) -PauseTrigger = media_player_ns.class_("PauseTrigger", automation.Trigger.template()) -AnnoucementTrigger = media_player_ns.class_( - "AnnouncementTrigger", automation.Trigger.template() +# Command actions that all share the same schema and codegen handler +_COMMAND_ACTIONS = [ + "play", + "pause", + "stop", + "toggle", + "volume_up", + "volume_down", + "turn_on", + "turn_off", + "next", + "previous", + "mute", + "unmute", + "repeat_off", + "repeat_one", + "repeat_all", + "shuffle", + "unshuffle", + "group_join", + "clear_playlist", +] + +# State triggers: (config_key, C++ class name) +_STATE_TRIGGERS = [ + (CONF_ON_STATE, "StateTrigger"), + (CONF_ON_IDLE, "IdleTrigger"), + (CONF_ON_PLAY, "PlayTrigger"), + (CONF_ON_PAUSE, "PauseTrigger"), + (CONF_ON_ANNOUNCEMENT, "AnnouncementTrigger"), + (CONF_ON_TURN_ON, "OnTrigger"), + (CONF_ON_TURN_OFF, "OffTrigger"), +] + +# State conditions that all share the same schema and codegen handler +_STATE_CONDITIONS = [ + "idle", + "paused", + "playing", + "announcing", + "on", + "off", + "muted", +] + +# Special action classes with custom schemas/handlers +PlayMediaAction = media_player_ns.class_( + "PlayMediaAction", automation.Action, cg.Parented.template(MediaPlayer) ) -OnTrigger = media_player_ns.class_("OnTrigger", automation.Trigger.template()) -OffTrigger = media_player_ns.class_("OffTrigger", automation.Trigger.template()) -IsIdleCondition = media_player_ns.class_("IsIdleCondition", automation.Condition) -IsPausedCondition = media_player_ns.class_("IsPausedCondition", automation.Condition) -IsPlayingCondition = media_player_ns.class_("IsPlayingCondition", automation.Condition) -IsAnnouncingCondition = media_player_ns.class_( - "IsAnnouncingCondition", automation.Condition +VolumeSetAction = media_player_ns.class_( + "VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer) ) -IsOnCondition = media_player_ns.class_("IsOnCondition", automation.Condition) -IsOffCondition = media_player_ns.class_("IsOffCondition", automation.Condition) async def setup_media_player_core_(var, config): await setup_entity(var, config, "media_player") - for conf in config.get(CONF_ON_STATE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_IDLE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_PLAY, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_PAUSE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_ANNOUNCEMENT, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_TURN_ON, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_TURN_OFF, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + for conf_key, _ in _STATE_TRIGGERS: + for conf in config.get(conf_key, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) async def register_media_player(var, config): @@ -133,41 +120,14 @@ async def new_media_player(config, *args): _MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( { - cv.Optional(CONF_ON_STATE): automation.validate_automation( + cv.Optional(conf_key): automation.validate_automation( { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + media_player_ns.class_(class_name, automation.Trigger.template()) + ), } - ), - cv.Optional(CONF_ON_IDLE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger), - } - ), - cv.Optional(CONF_ON_PLAY): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PlayTrigger), - } - ), - cv.Optional(CONF_ON_PAUSE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger), - } - ), - cv.Optional(CONF_ON_ANNOUNCEMENT): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(AnnoucementTrigger), - } - ), - cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnTrigger), - } - ), - cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OffTrigger), - } - ), + ) + for conf_key, class_name in _STATE_TRIGGERS } ) @@ -228,56 +188,48 @@ async def media_player_play_media_action(config, action_id, template_arg, args): return var -@automation.register_action("media_player.play", PlayAction, MEDIA_PLAYER_ACTION_SCHEMA) -@automation.register_action( - "media_player.toggle", ToggleAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action( - "media_player.pause", PauseAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action("media_player.stop", StopAction, MEDIA_PLAYER_ACTION_SCHEMA) -@automation.register_action( - "media_player.volume_up", VolumeUpAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action( - "media_player.volume_down", VolumeDownAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action( - "media_player.turn_on", TurnOnAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action( - "media_player.turn_off", TurnOffAction, MEDIA_PLAYER_ACTION_SCHEMA -) -async def media_player_action(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_) - cg.add(var.set_announcement(announcement)) - return var +def _snake_to_camel(name): + return "".join(word.capitalize() for word in name.split("_")) -@automation.register_condition( - "media_player.is_idle", IsIdleCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_paused", IsPausedCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_playing", IsPlayingCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_announcing", IsAnnouncingCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_on", IsOnCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_off", IsOffCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -async def media_player_condition(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var +def _register_command_actions(): + async def handler(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_) + cg.add(var.set_announcement(announcement)) + return var + + for action_name in _COMMAND_ACTIONS: + class_name = f"{_snake_to_camel(action_name)}Action" + action_class = media_player_ns.class_( + class_name, automation.Action, cg.Parented.template(MediaPlayer) + ) + automation.register_action( + f"media_player.{action_name}", action_class, MEDIA_PLAYER_ACTION_SCHEMA + )(handler) + + +_register_command_actions() + + +def _register_state_conditions(): + async def handler(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + for condition_name in _STATE_CONDITIONS: + class_name = f"Is{_snake_to_camel(condition_name)}Condition" + condition_class = media_player_ns.class_(class_name, automation.Condition) + automation.register_condition( + f"media_player.is_{condition_name}", + condition_class, + MEDIA_PLAYER_CONDITION_SCHEMA, + )(handler) + + +_register_state_conditions() @automation.register_action( diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 50e7693cb5..90e7bf75b5 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -32,6 +32,28 @@ template using TurnOnAction = MediaPlayerCommandAction; template using TurnOffAction = MediaPlayerCommandAction; +template +using NextAction = MediaPlayerCommandAction; +template +using PreviousAction = MediaPlayerCommandAction; +template +using MuteAction = MediaPlayerCommandAction; +template +using UnmuteAction = MediaPlayerCommandAction; +template +using RepeatOffAction = MediaPlayerCommandAction; +template +using RepeatOneAction = MediaPlayerCommandAction; +template +using RepeatAllAction = MediaPlayerCommandAction; +template +using ShuffleAction = MediaPlayerCommandAction; +template +using UnshuffleAction = MediaPlayerCommandAction; +template +using GroupJoinAction = MediaPlayerCommandAction; +template +using ClearPlaylistAction = MediaPlayerCommandAction; template class PlayMediaAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, media_url) @@ -105,5 +127,10 @@ template class IsOffCondition : public Condition, public bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_OFF; } }; +template class IsMutedCondition : public Condition, public Parented { + public: + bool check(const Ts &...x) override { return this->parent_->is_muted(); } +}; + } // namespace media_player } // namespace esphome diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index 17d9b054da..a53d598b0f 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -60,11 +60,39 @@ const char *media_player_command_to_string(MediaPlayerCommand command) { return "TURN_ON"; case MEDIA_PLAYER_COMMAND_TURN_OFF: return "TURN_OFF"; + case MEDIA_PLAYER_COMMAND_NEXT: + return "NEXT"; + case MEDIA_PLAYER_COMMAND_PREVIOUS: + return "PREVIOUS"; + case MEDIA_PLAYER_COMMAND_REPEAT_ALL: + return "REPEAT_ALL"; + case MEDIA_PLAYER_COMMAND_SHUFFLE: + return "SHUFFLE"; + case MEDIA_PLAYER_COMMAND_UNSHUFFLE: + return "UNSHUFFLE"; + case MEDIA_PLAYER_COMMAND_GROUP_JOIN: + return "GROUP_JOIN"; default: return "UNKNOWN"; } } +void MediaPlayerTraits::set_supports_pause(bool supports_pause) { + if (supports_pause) { + this->feature_flags_ |= MediaPlayerEntityFeature::PAUSE | MediaPlayerEntityFeature::PLAY; + } else { + this->feature_flags_ &= ~(MediaPlayerEntityFeature::PAUSE | MediaPlayerEntityFeature::PLAY); + } +} + +void MediaPlayerTraits::set_supports_turn_off_on(bool supports_turn_off_on) { + if (supports_turn_off_on) { + this->feature_flags_ |= MediaPlayerEntityFeature::TURN_OFF | MediaPlayerEntityFeature::TURN_ON; + } else { + this->feature_flags_ &= ~(MediaPlayerEntityFeature::TURN_OFF | MediaPlayerEntityFeature::TURN_ON); + } +} + void MediaPlayerCall::validate_() { if (this->media_url_.has_value()) { if (this->command_.has_value() && this->command_.value() != MEDIA_PLAYER_COMMAND_ENQUEUE) { @@ -125,6 +153,30 @@ MediaPlayerCall &MediaPlayerCall::set_command(const char *command) { this->set_command(MEDIA_PLAYER_COMMAND_TURN_ON); } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TURN_OFF")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_TURN_OFF); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("VOLUME_UP")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_VOLUME_UP); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("VOLUME_DOWN")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_VOLUME_DOWN); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("ENQUEUE")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_ENQUEUE); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("REPEAT_ONE")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_REPEAT_ONE); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("REPEAT_OFF")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_REPEAT_OFF); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("REPEAT_ALL")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_REPEAT_ALL); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("CLEAR_PLAYLIST")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("NEXT")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_NEXT); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("PREVIOUS")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_PREVIOUS); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("SHUFFLE")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_SHUFFLE); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("UNSHUFFLE")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_UNSHUFFLE); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("GROUP_JOIN")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_GROUP_JOIN); } else { ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command); } diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h index f75a68dd85..3509747718 100644 --- a/esphome/components/media_player/media_player.h +++ b/esphome/components/media_player/media_player.h @@ -58,6 +58,12 @@ enum MediaPlayerCommand : uint8_t { MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST = 11, MEDIA_PLAYER_COMMAND_TURN_ON = 12, MEDIA_PLAYER_COMMAND_TURN_OFF = 13, + MEDIA_PLAYER_COMMAND_NEXT = 14, + MEDIA_PLAYER_COMMAND_PREVIOUS = 15, + MEDIA_PLAYER_COMMAND_REPEAT_ALL = 16, + MEDIA_PLAYER_COMMAND_SHUFFLE = 17, + MEDIA_PLAYER_COMMAND_UNSHUFFLE = 18, + MEDIA_PLAYER_COMMAND_GROUP_JOIN = 19, }; const char *media_player_command_to_string(MediaPlayerCommand command); @@ -74,38 +80,40 @@ struct MediaPlayerSupportedFormat { uint32_t sample_bytes; }; +// Base features always reported for all media players +static constexpr uint32_t BASE_MEDIA_PLAYER_FEATURES = + MediaPlayerEntityFeature::PLAY_MEDIA | MediaPlayerEntityFeature::BROWSE_MEDIA | MediaPlayerEntityFeature::STOP | + MediaPlayerEntityFeature::VOLUME_SET | MediaPlayerEntityFeature::VOLUME_MUTE | + MediaPlayerEntityFeature::MEDIA_ANNOUNCE; + class MediaPlayer; class MediaPlayerTraits { public: MediaPlayerTraits() = default; - void set_supports_pause(bool supports_pause) { this->supports_pause_ = supports_pause; } - bool get_supports_pause() const { return this->supports_pause_; } - - void set_supports_turn_off_on(bool supports_turn_off_on) { this->supports_turn_off_on_ = supports_turn_off_on; } - bool get_supports_turn_off_on() const { return this->supports_turn_off_on_; } + uint32_t get_feature_flags() const { return this->feature_flags_; } + void add_feature_flags(uint32_t feature_flags) { this->feature_flags_ |= feature_flags; } + void clear_feature_flags(uint32_t feature_flags) { this->feature_flags_ &= ~feature_flags; } + // Returns true only if all specified flags are set + bool has_feature_flags(uint32_t feature_flags) const { + return (this->feature_flags_ & feature_flags) == feature_flags; + } std::vector &get_supported_formats() { return this->supported_formats_; } - uint32_t get_feature_flags() const { - uint32_t flags = 0; - flags |= MediaPlayerEntityFeature::PLAY_MEDIA | MediaPlayerEntityFeature::BROWSE_MEDIA | - MediaPlayerEntityFeature::STOP | MediaPlayerEntityFeature::VOLUME_SET | - MediaPlayerEntityFeature::VOLUME_MUTE | MediaPlayerEntityFeature::MEDIA_ANNOUNCE; - if (this->get_supports_pause()) { - flags |= MediaPlayerEntityFeature::PAUSE | MediaPlayerEntityFeature::PLAY; - } - if (this->get_supports_turn_off_on()) { - flags |= MediaPlayerEntityFeature::TURN_OFF | MediaPlayerEntityFeature::TURN_ON; - } - return flags; + // Legacy setters/getters are kept for backward compatibility + void set_supports_pause(bool supports_pause); + bool get_supports_pause() const { return this->has_feature_flags(MediaPlayerEntityFeature::PAUSE); } + + void set_supports_turn_off_on(bool supports_turn_off_on); + bool get_supports_turn_off_on() const { + return this->has_feature_flags(MediaPlayerEntityFeature::TURN_ON | MediaPlayerEntityFeature::TURN_OFF); } protected: std::vector supported_formats_{}; - bool supports_pause_{false}; - bool supports_turn_off_on_{false}; + uint32_t feature_flags_{BASE_MEDIA_PLAYER_FEATURES}; }; class MediaPlayerCall { diff --git a/tests/components/media_player/common.yaml b/tests/components/media_player/common.yaml index 763bc231c0..c83ee89ad4 100644 --- a/tests/components/media_player/common.yaml +++ b/tests/components/media_player/common.yaml @@ -23,8 +23,27 @@ media_player: - media_player.stop: - media_player.stop: announcement: true + on_announcement: + - media_player.play: + on_turn_on: + - media_player.play: + on_turn_off: + - media_player.stop: on_pause: - media_player.toggle: + - media_player.turn_on: + - media_player.turn_off: + - media_player.next: + - media_player.previous: + - media_player.mute: + - media_player.unmute: + - media_player.repeat_off: + - media_player.repeat_one: + - media_player.repeat_all: + - media_player.shuffle: + - media_player.unshuffle: + - media_player.group_join: + - media_player.clear_playlist: - wait_until: media_player.is_idle: - wait_until: @@ -33,6 +52,12 @@ media_player: media_player.is_announcing: - wait_until: media_player.is_paused: + - wait_until: + media_player.is_on: + - wait_until: + media_player.is_off: + - wait_until: + media_player.is_muted: - media_player.volume_up: - media_player.volume_down: - media_player.volume_set: 50% From ba7134ee3f870ce00d2990d84b142b734028330f Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 18 Feb 2026 21:51:16 -0600 Subject: [PATCH 35/43] [mdns] add Sendspin advertisement support (#14013) Co-authored-by: J. Nick Koston --- esphome/components/mdns/__init__.py | 2 +- esphome/components/mdns/mdns_component.cpp | 19 ++++++++++++++++++- esphome/core/defines.h | 2 ++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 3088d8ad7e..f87f929615 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -21,7 +21,7 @@ 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") +COMPONENTS_WITH_MDNS_SERVICES = ("api", "prometheus", "sendspin", "web_server") mdns_ns = cg.esphome_ns.namespace("mdns") MDNSComponent = mdns_ns.class_("MDNSComponent", cg.Component) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 47db92610a..5e5e1279d9 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -29,6 +29,10 @@ static const char *const TAG = "mdns"; #define USE_WEBSERVER_PORT 80 // NOLINT #endif +#ifndef USE_SENDSPIN_PORT +#define USE_SENDSPIN_PORT 8928 // NOLINT +#endif + // Define all constant strings using the macro MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp"); @@ -150,6 +154,18 @@ void MDNSComponent::compile_records_(StaticVector Date: Wed, 18 Feb 2026 21:51:33 -0600 Subject: [PATCH 36/43] [audio, speaker] Add support for decoding Ogg Opus files (#13967) --- esphome/components/audio/__init__.py | 42 ++++++++++ esphome/components/audio/audio.cpp | 4 + esphome/components/audio/audio.h | 3 + esphome/components/audio/audio_decoder.cpp | 60 +++++++++++++- esphome/components/audio/audio_decoder.h | 15 +++- esphome/components/audio/audio_reader.cpp | 13 +++ .../speaker/media_player/__init__.py | 82 ++++++++++++++++--- .../speaker/media_player/audio_pipeline.cpp | 10 +++ esphome/core/defines.h | 1 + esphome/idf_component.yml | 2 + .../speaker/audio_dac.esp32-ard.yaml | 10 --- .../speaker/common-media_player.yaml | 7 ++ .../speaker/media_player.esp32-s3-idf.yaml | 9 -- ...idf.yaml => test-audio_dac.esp32-idf.yaml} | 0 ....yaml => test-media_player.esp32-idf.yaml} | 0 tests/components/speaker/test.esp32-ard.yaml | 10 --- 16 files changed, 222 insertions(+), 46 deletions(-) delete mode 100644 tests/components/speaker/audio_dac.esp32-ard.yaml delete mode 100644 tests/components/speaker/media_player.esp32-s3-idf.yaml rename tests/components/speaker/{audio_dac.esp32-idf.yaml => test-audio_dac.esp32-idf.yaml} (100%) rename tests/components/speaker/{media_player.esp32-idf.yaml => test-media_player.esp32-idf.yaml} (100%) delete mode 100644 tests/components/speaker/test.esp32-ard.yaml diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index f48b776ddd..d8d426ec63 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -1,10 +1,14 @@ +from dataclasses import dataclass + import esphome.codegen as cg from esphome.components.esp32 import add_idf_component, include_builtin_idf_component import esphome.config_validation as cv from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE +from esphome.core import CORE import esphome.final_validate as fv CODEOWNERS = ["@kahrendt"] +DOMAIN = "audio" audio_ns = cg.esphome_ns.namespace("audio") AudioFile = audio_ns.struct("AudioFile") @@ -14,9 +18,38 @@ AUDIO_FILE_TYPE_ENUM = { "WAV": AudioFileType.WAV, "MP3": AudioFileType.MP3, "FLAC": AudioFileType.FLAC, + "OPUS": AudioFileType.OPUS, } +@dataclass +class AudioData: + flac_support: bool = False + mp3_support: bool = False + opus_support: bool = False + + +def _get_data() -> AudioData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = AudioData() + return CORE.data[DOMAIN] + + +def request_flac_support() -> None: + """Request FLAC codec support for audio decoding.""" + _get_data().flac_support = True + + +def request_mp3_support() -> None: + """Request MP3 codec support for audio decoding.""" + _get_data().mp3_support = True + + +def request_opus_support() -> None: + """Request Opus codec support for audio decoding.""" + _get_data().opus_support = True + + CONF_MIN_BITS_PER_SAMPLE = "min_bits_per_sample" CONF_MAX_BITS_PER_SAMPLE = "max_bits_per_sample" CONF_MIN_CHANNELS = "min_channels" @@ -173,3 +206,12 @@ async def to_code(config): name="esphome/esp-audio-libs", ref="2.0.3", ) + + data = _get_data() + if data.flac_support: + cg.add_define("USE_AUDIO_FLAC_SUPPORT") + if data.mp3_support: + cg.add_define("USE_AUDIO_MP3_SUPPORT") + if data.opus_support: + cg.add_define("USE_AUDIO_OPUS_SUPPORT") + add_idf_component(name="esphome/micro-opus", ref="0.3.3") diff --git a/esphome/components/audio/audio.cpp b/esphome/components/audio/audio.cpp index 9cc9b7d0da..40592f6107 100644 --- a/esphome/components/audio/audio.cpp +++ b/esphome/components/audio/audio.cpp @@ -46,6 +46,10 @@ const char *audio_file_type_to_string(AudioFileType file_type) { #ifdef USE_AUDIO_MP3_SUPPORT case AudioFileType::MP3: return "MP3"; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case AudioFileType::OPUS: + return "OPUS"; #endif case AudioFileType::WAV: return "WAV"; diff --git a/esphome/components/audio/audio.h b/esphome/components/audio/audio.h index e01d7eb101..7d7db9e944 100644 --- a/esphome/components/audio/audio.h +++ b/esphome/components/audio/audio.h @@ -112,6 +112,9 @@ enum class AudioFileType : uint8_t { #endif #ifdef USE_AUDIO_MP3_SUPPORT MP3, +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + OPUS, #endif WAV, }; diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index 8f514468c4..ee6d7d0a15 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -3,10 +3,13 @@ #ifdef USE_ESP32 #include "esphome/core/hal.h" +#include "esphome/core/log.h" namespace esphome { namespace audio { +static const char *const TAG = "audio.decoder"; + static const uint32_t DECODING_TIMEOUT_MS = 50; // The decode function will yield after this duration static const uint32_t READ_WRITE_TIMEOUT_MS = 20; // Timeout for transferring audio data @@ -79,6 +82,14 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { // Always reallocate the output transfer buffer to the smallest necessary size this->output_transfer_buffer_->reallocate(this->free_buffer_required_); break; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case AudioFileType::OPUS: + this->opus_decoder_ = make_unique(); + this->free_buffer_required_ = + this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header + this->decoder_buffers_internally_ = true; + break; #endif case AudioFileType::WAV: this->wav_decoder_ = make_unique(); @@ -158,8 +169,9 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { // Decode more audio // Only shift data on the first loop iteration to avoid unnecessary, slow moves - size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), - first_loop_iteration); + // If the decoder buffers internally, then never shift + size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source( + pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), first_loop_iteration && !this->decoder_buffers_internally_); if (!first_loop_iteration && (this->input_transfer_buffer_->available() < bytes_processed)) { // Less data is available than what was processed in last iteration, so don't attempt to decode. @@ -195,6 +207,11 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { case AudioFileType::MP3: state = this->decode_mp3_(); break; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case AudioFileType::OPUS: + state = this->decode_opus_(); + break; #endif case AudioFileType::WAV: state = this->decode_wav_(); @@ -339,6 +356,45 @@ FileDecoderState AudioDecoder::decode_mp3_() { } #endif +#ifdef USE_AUDIO_OPUS_SUPPORT +FileDecoderState AudioDecoder::decode_opus_() { + bool processed_header = this->opus_decoder_->is_initialized(); + + size_t bytes_consumed, samples_decoded; + + micro_opus::OggOpusResult result = this->opus_decoder_->decode( + this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(), + this->output_transfer_buffer_->get_buffer_end(), this->output_transfer_buffer_->free(), bytes_consumed, + samples_decoded); + + if (result == micro_opus::OGG_OPUS_OK) { + if (!processed_header && this->opus_decoder_->is_initialized()) { + // Header processed and stream info is available + this->audio_stream_info_ = + audio::AudioStreamInfo(this->opus_decoder_->get_bit_depth(), this->opus_decoder_->get_channels(), + this->opus_decoder_->get_sample_rate()); + } + if (samples_decoded > 0 && this->audio_stream_info_.has_value()) { + // Some audio was processed + this->output_transfer_buffer_->increase_buffer_length( + this->audio_stream_info_.value().frames_to_bytes(samples_decoded)); + } + this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); + } else if (result == micro_opus::OGG_OPUS_OUTPUT_BUFFER_TOO_SMALL) { + // Reallocate to decode the packet on the next call + this->free_buffer_required_ = this->opus_decoder_->get_required_output_buffer_size(); + if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) { + // Couldn't reallocate output buffer + return FileDecoderState::FAILED; + } + } else { + ESP_LOGE(TAG, "Opus decoder failed: %" PRId8, result); + return FileDecoderState::POTENTIALLY_FAILED; + } + return FileDecoderState::MORE_TO_PROCESS; +} +#endif + FileDecoderState AudioDecoder::decode_wav_() { if (!this->audio_stream_info_.has_value()) { // Header hasn't been processed diff --git a/esphome/components/audio/audio_decoder.h b/esphome/components/audio/audio_decoder.h index 2ca1d623fe..cad16110ae 100644 --- a/esphome/components/audio/audio_decoder.h +++ b/esphome/components/audio/audio_decoder.h @@ -24,6 +24,11 @@ #endif #include +// micro-opus +#ifdef USE_AUDIO_OPUS_SUPPORT +#include +#endif + namespace esphome { namespace audio { @@ -47,7 +52,7 @@ class AudioDecoder { * @brief Class that facilitates decoding an audio file. * The audio file is read from a ring buffer source, decoded, and sent to an audio sink (ring buffer or speaker * component). - * Supports wav, flac, and mp3 formats. + * Supports wav, flac, mp3, and ogg opus formats. */ public: /// @brief Allocates the input and output transfer buffers @@ -55,7 +60,7 @@ class AudioDecoder { /// @param output_buffer_size Size of the output transfer buffer in bytes. AudioDecoder(size_t input_buffer_size, size_t output_buffer_size); - /// @brief Deallocates the MP3 decoder (the flac and wav decoders are deallocated automatically) + /// @brief Deallocates the MP3 decoder (the flac, opus, and wav decoders are deallocated automatically) ~AudioDecoder(); /// @brief Adds a source ring buffer for raw file data. Takes ownership of the ring buffer in a shared_ptr. @@ -108,6 +113,10 @@ class AudioDecoder { #ifdef USE_AUDIO_MP3_SUPPORT FileDecoderState decode_mp3_(); esp_audio_libs::helix_decoder::HMP3Decoder mp3_decoder_; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + FileDecoderState decode_opus_(); + std::unique_ptr opus_decoder_; #endif FileDecoderState decode_wav_(); @@ -124,6 +133,8 @@ class AudioDecoder { bool end_of_file_{false}; bool wav_has_known_end_{false}; + bool decoder_buffers_internally_{false}; + bool pause_output_{false}; uint32_t accumulated_frames_written_{0}; diff --git a/esphome/components/audio/audio_reader.cpp b/esphome/components/audio/audio_reader.cpp index 4e4bd31f9b..78d69d7a39 100644 --- a/esphome/components/audio/audio_reader.cpp +++ b/esphome/components/audio/audio_reader.cpp @@ -197,6 +197,11 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { else if (str_endswith_ignore_case(url, ".flac")) { file_type = AudioFileType::FLAC; } +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + else if (str_endswith_ignore_case(url, ".opus")) { + file_type = AudioFileType::OPUS; + } #endif else { file_type = AudioFileType::NONE; @@ -241,6 +246,14 @@ AudioFileType AudioReader::get_audio_type(const char *content_type) { if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) { return AudioFileType::FLAC; } +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + // Match "audio/ogg" with a codecs parameter containing "opus" + // Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc. + // Plain "audio/ogg" without a codecs parameter is not matched, as those are almost always Ogg Vorbis streams + if (strncasecmp(content_type, "audio/ogg", 9) == 0 && strcasestr(content_type + 9, "opus") != nullptr) { + return AudioFileType::OPUS; + } #endif return AudioFileType::NONE; } diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 034312236c..b302bd9b23 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -26,7 +26,6 @@ from esphome.const import ( from esphome.core import CORE, HexInt from esphome.core.entity_helpers import inherit_property_from from esphome.external_files import download_content -from esphome.final_validate import full_config _LOGGER = logging.getLogger(__name__) @@ -37,6 +36,10 @@ DEPENDENCIES = ["network"] CODEOWNERS = ["@kahrendt", "@synesthesiam"] DOMAIN = "media_player" +CODEC_SUPPORT_ALL = "all" +CODEC_SUPPORT_NEEDED = "needed" +CODEC_SUPPORT_NONE = "none" + TYPE_LOCAL = "local" TYPE_WEB = "web" @@ -110,6 +113,8 @@ def _get_supported_format_struct(pipeline, type): args.append(("format", "flac")) elif pipeline[CONF_FORMAT] == "MP3": args.append(("format", "mp3")) + elif pipeline[CONF_FORMAT] == "OPUS": + args.append(("format", "opus")) elif pipeline[CONF_FORMAT] == "WAV": args.append(("format", "wav")) @@ -173,6 +178,13 @@ def _read_audio_file_and_type(file_config): media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"] elif file_type in ("flac"): media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"] + elif ( + file_type in ("ogg") + and len(data) >= 36 + and data.startswith(b"OggS") + and data[28:36] == b"OpusHead" + ): + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["OPUS"] return data, media_file_type @@ -199,6 +211,10 @@ def _validate_pipeline(config): inherit_property_from(CONF_NUM_CHANNELS, CONF_SPEAKER)(config) inherit_property_from(CONF_SAMPLE_RATE, CONF_SPEAKER)(config) + # Opus only supports 48 kHz + if config.get(CONF_FORMAT) == "OPUS" and config.get(CONF_SAMPLE_RATE) != 48000: + raise cv.Invalid("Opus only supports a sample rate of 48000 Hz") + # Validate the transcoder settings is compatible with the speaker audio.final_validate_audio_schema( "speaker media_player", @@ -225,12 +241,27 @@ def _validate_repeated_speaker(config): def _final_validate(config): - # Default to using codec if psram is enabled - if (use_codec := config.get(CONF_CODEC_SUPPORT_ENABLED)) is None: - use_codec = psram.DOMAIN in full_config.get() - conf_id = config[CONF_ID].id - core_data = CORE.data.setdefault(DOMAIN, {conf_id: {}}) - core_data[conf_id][CONF_CODEC_SUPPORT_ENABLED] = use_codec + # Normalize boolean values to string equivalents + codec_mode = config[CONF_CODEC_SUPPORT_ENABLED] + if codec_mode is True: + codec_mode = CODEC_SUPPORT_ALL + elif codec_mode is False: + codec_mode = CODEC_SUPPORT_NONE + + use_codec = codec_mode != CODEC_SUPPORT_NONE + + # In "needed" mode, collect formats from pipelines and files + needed_formats = set() + need_all = False + if codec_mode == CODEC_SUPPORT_NEEDED: + for pipeline_key in (CONF_ANNOUNCEMENT_PIPELINE, CONF_MEDIA_PIPELINE): + if pipeline := config.get(pipeline_key): + fmt = pipeline[CONF_FORMAT] + if fmt == "NONE": + # No preferred format means any format could arrive + need_all = True + else: + needed_formats.add(fmt) for file_config in config.get(CONF_FILES, []): _, media_file_type = _read_audio_file_and_type(file_config) @@ -243,6 +274,26 @@ def _final_validate(config): raise cv.Invalid( f"Unsupported local media file type, set {CONF_CODEC_SUPPORT_ENABLED} to true or convert the media file to wav" ) + # In "needed" mode, add file format to needed codecs + if codec_mode == CODEC_SUPPORT_NEEDED: + for fmt_name, fmt_enum in audio.AUDIO_FILE_TYPE_ENUM.items(): + if str(media_file_type) == str(fmt_enum): + if fmt_name not in ("WAV", "NONE"): + needed_formats.add(fmt_name) + break + + # Request codec support + if codec_mode == CODEC_SUPPORT_ALL or need_all: + audio.request_flac_support() + audio.request_mp3_support() + audio.request_opus_support() + elif codec_mode == CODEC_SUPPORT_NEEDED: + if "FLAC" in needed_formats: + audio.request_flac_support() + if "MP3" in needed_formats: + audio.request_mp3_support() + if "OPUS" in needed_formats: + audio.request_opus_support() return config @@ -307,7 +358,17 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range( min=4000, max=4000000 ), - cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.boolean, + cv.Optional( + CONF_CODEC_SUPPORT_ENABLED, default=CODEC_SUPPORT_NEEDED + ): cv.Any( + cv.boolean, + cv.one_of( + CODEC_SUPPORT_ALL, + CODEC_SUPPORT_NEEDED, + CODEC_SUPPORT_NONE, + lower=True, + ), + ), cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All( cv.boolean, cv.requires_component(psram.DOMAIN) @@ -340,11 +401,6 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): - if CORE.data[DOMAIN][config[CONF_ID].id][CONF_CODEC_SUPPORT_ENABLED]: - # Compile all supported audio codecs - cg.add_define("USE_AUDIO_FLAC_SUPPORT", True) - cg.add_define("USE_AUDIO_MP3_SUPPORT", True) - var = await media_player.new_media_player(config) await cg.register_component(var, config) diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 8be37d740a..177743feb1 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -13,7 +13,12 @@ namespace speaker { static const uint32_t INITIAL_BUFFER_MS = 1000; // Start playback after buffering this duration of the file static const uint32_t READ_TASK_STACK_SIZE = 5 * 1024; +// Opus decoding uses more stack than other codecs +#ifdef USE_AUDIO_OPUS_SUPPORT +static const uint32_t DECODE_TASK_STACK_SIZE = 5 * 1024; +#else static const uint32_t DECODE_TASK_STACK_SIZE = 3 * 1024; +#endif static const uint32_t INFO_ERROR_QUEUE_COUNT = 5; @@ -552,6 +557,11 @@ void AudioPipeline::decode_task(void *params) { case audio::AudioFileType::FLAC: initial_bytes_to_buffer /= 2; // Estimate the FLAC compression factor is 2 break; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case audio::AudioFileType::OPUS: + initial_bytes_to_buffer /= 8; // Estimate the Opus compression factor is 8 + break; #endif default: break; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a70c6cd0d0..5559ec88d0 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -130,6 +130,7 @@ #define USE_AUDIO_DAC #define USE_AUDIO_FLAC_SUPPORT #define USE_AUDIO_MP3_SUPPORT +#define USE_AUDIO_OPUS_SUPPORT #define USE_API #define USE_API_CLIENT_CONNECTED_TRIGGER #define USE_API_CLIENT_DISCONNECTED_TRIGGER diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index f39ea9b3ae..83b2d9d95c 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -3,6 +3,8 @@ dependencies: version: "7.4.2" esphome/esp-audio-libs: version: 2.0.3 + esphome/micro-opus: + version: 0.3.3 espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: diff --git a/tests/components/speaker/audio_dac.esp32-ard.yaml b/tests/components/speaker/audio_dac.esp32-ard.yaml deleted file mode 100644 index 3f5d1bba7c..0000000000 --- a/tests/components/speaker/audio_dac.esp32-ard.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO27 - i2s_lrclk_pin: GPIO26 - i2s_mclk_pin: GPIO25 - i2s_dout_pin: GPIO23 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml - -<<: !include common-audio_dac.yaml diff --git a/tests/components/speaker/common-media_player.yaml b/tests/components/speaker/common-media_player.yaml index edc9f670fc..c958c0d912 100644 --- a/tests/components/speaker/common-media_player.yaml +++ b/tests/components/speaker/common-media_player.yaml @@ -1,5 +1,11 @@ <<: !include common.yaml +wifi: + ap: + +psram: + mode: quad + media_player: - platform: speaker id: speaker_media_player_id @@ -10,3 +16,4 @@ media_player: volume_max: 0.95 volume_min: 0.0 task_stack_in_psram: true + codec_support_enabled: all diff --git a/tests/components/speaker/media_player.esp32-s3-idf.yaml b/tests/components/speaker/media_player.esp32-s3-idf.yaml deleted file mode 100644 index b3eec04d23..0000000000 --- a/tests/components/speaker/media_player.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO2 - sda_pin: GPIO3 - i2s_bclk_pin: GPIO4 - i2s_lrclk_pin: GPIO5 - i2s_mclk_pin: GPIO6 - i2s_dout_pin: GPIO7 - -<<: !include common-media_player.yaml diff --git a/tests/components/speaker/audio_dac.esp32-idf.yaml b/tests/components/speaker/test-audio_dac.esp32-idf.yaml similarity index 100% rename from tests/components/speaker/audio_dac.esp32-idf.yaml rename to tests/components/speaker/test-audio_dac.esp32-idf.yaml diff --git a/tests/components/speaker/media_player.esp32-idf.yaml b/tests/components/speaker/test-media_player.esp32-idf.yaml similarity index 100% rename from tests/components/speaker/media_player.esp32-idf.yaml rename to tests/components/speaker/test-media_player.esp32-idf.yaml diff --git a/tests/components/speaker/test.esp32-ard.yaml b/tests/components/speaker/test.esp32-ard.yaml deleted file mode 100644 index 13350cd097..0000000000 --- a/tests/components/speaker/test.esp32-ard.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO27 - i2s_lrclk_pin: GPIO26 - i2s_mclk_pin: GPIO25 - i2s_dout_pin: GPIO4 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml - -<<: !include common.yaml From 4d05e4d5765c037f234ddebb8442af2007c4fc60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20H=C3=B6rsken?= Date: Thu, 19 Feb 2026 04:52:38 +0100 Subject: [PATCH 37/43] [esp32_camera] Add support for sensors without JPEG support (#9496) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/esp32_camera/__init__.py | 50 ++++- .../components/esp32_camera/esp32_camera.cpp | 207 +++++++++++++----- .../components/esp32_camera/esp32_camera.h | 14 ++ esphome/core/defines.h | 1 + .../common/i2c_camera/esp32-idf.yaml | 1 + 5 files changed, 212 insertions(+), 61 deletions(-) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index db6244fb3f..3a5d87792b 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -22,8 +22,10 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VSYNC_PIN, ) +from esphome.core import CORE from esphome.core.entity_helpers import setup_entity import esphome.final_validate as fv +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -84,6 +86,18 @@ FRAME_SIZES = { "2560X1920": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1920, "QSXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1920, } +ESP32CameraPixelFormat = esp32_camera_ns.enum("ESP32CameraPixelFormat") +PIXEL_FORMATS = { + "RGB565": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB565, + "YUV422": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_YUV422, + "YUV420": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_YUV420, + "GRAYSCALE": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_GRAYSCALE, + "JPEG": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_JPEG, + "RGB888": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB888, + "RAW": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RAW, + "RGB444": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB444, + "RGB555": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB555, +} ESP32GainControlMode = esp32_camera_ns.enum("ESP32GainControlMode") ENUM_GAIN_CONTROL_MODE = { "MANUAL": ESP32GainControlMode.ESP32_GC_MODE_MANU, @@ -131,6 +145,7 @@ CONF_EXTERNAL_CLOCK = "external_clock" CONF_I2C_PINS = "i2c_pins" CONF_POWER_DOWN_PIN = "power_down_pin" # image +CONF_PIXEL_FORMAT = "pixel_format" CONF_JPEG_QUALITY = "jpeg_quality" CONF_VERTICAL_FLIP = "vertical_flip" CONF_HORIZONTAL_MIRROR = "horizontal_mirror" @@ -171,6 +186,21 @@ def validate_fb_location_(value): return validator(value) +def validate_jpeg_quality(config: ConfigType) -> ConfigType: + quality = config.get(CONF_JPEG_QUALITY) + pixel_format = config.get(CONF_PIXEL_FORMAT, "JPEG") + + if quality == 0: + # Set default JPEG quality if not specified for backwards compatibility + if pixel_format == "JPEG": + config[CONF_JPEG_QUALITY] = 10 + # For pixel formats other than JPEG, the valid 0 means no conversion + elif quality < 6 or quality > 63: + raise cv.Invalid(f"jpeg_quality must be between 6 and 63, got {quality}") + + return config + + CONFIG_SCHEMA = cv.All( cv.ENTITY_BASE_SCHEMA.extend( { @@ -206,7 +236,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum( FRAME_SIZES, upper=True ), - cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63), + cv.Optional(CONF_PIXEL_FORMAT, default="JPEG"): cv.enum( + PIXEL_FORMATS, upper=True + ), + cv.Optional(CONF_JPEG_QUALITY, default=0): cv.Any( + cv.one_of(0), cv.int_range(min=6, max=63) + ), cv.Optional(CONF_CONTRAST, default=0): camera_range_param, cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, cv.Optional(CONF_SATURATION, default=0): camera_range_param, @@ -270,11 +305,21 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), + validate_jpeg_quality, cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID), ) def _final_validate(config): + # Check psram requirement for non-JPEG formats + if ( + config.get(CONF_PIXEL_FORMAT, "JPEG") != "JPEG" + and psram_domain not in CORE.loaded_integrations + ): + raise cv.Invalid( + f"Non-JPEG pixel formats require the '{psram_domain}' component for JPEG conversion" + ) + if CONF_I2C_PINS not in config: return fconf = fv.full_config.get() @@ -298,6 +343,7 @@ SETTERS = { CONF_RESET_PIN: "set_reset_pin", CONF_POWER_DOWN_PIN: "set_power_down_pin", # image + CONF_PIXEL_FORMAT: "set_pixel_format", CONF_JPEG_QUALITY: "set_jpeg_quality", CONF_VERTICAL_FLIP: "set_vertical_flip", CONF_HORIZONTAL_MIRROR: "set_horizontal_mirror", @@ -351,6 +397,8 @@ async def to_code(config): cg.add(var.set_frame_size(config[CONF_RESOLUTION])) cg.add_define("USE_CAMERA") + if config[CONF_JPEG_QUALITY] != 0 and config[CONF_PIXEL_FORMAT] != "JPEG": + cg.add_define("USE_ESP32_CAMERA_JPEG_CONVERSION") add_idf_component(name="espressif/esp32-camera", ref="2.1.1") add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index cfe06b1673..655ae54f0a 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -16,6 +16,74 @@ static constexpr size_t FRAMEBUFFER_TASK_STACK_SIZE = 1792; static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000; #endif +static const char *frame_size_to_str(framesize_t size) { + switch (size) { + case FRAMESIZE_QQVGA: + return "160x120 (QQVGA)"; + case FRAMESIZE_QCIF: + return "176x155 (QCIF)"; + case FRAMESIZE_HQVGA: + return "240x176 (HQVGA)"; + case FRAMESIZE_QVGA: + return "320x240 (QVGA)"; + case FRAMESIZE_CIF: + return "400x296 (CIF)"; + case FRAMESIZE_VGA: + return "640x480 (VGA)"; + case FRAMESIZE_SVGA: + return "800x600 (SVGA)"; + case FRAMESIZE_XGA: + return "1024x768 (XGA)"; + case FRAMESIZE_SXGA: + return "1280x1024 (SXGA)"; + case FRAMESIZE_UXGA: + return "1600x1200 (UXGA)"; + case FRAMESIZE_FHD: + return "1920x1080 (FHD)"; + case FRAMESIZE_P_HD: + return "720x1280 (P_HD)"; + case FRAMESIZE_P_3MP: + return "864x1536 (P_3MP)"; + case FRAMESIZE_QXGA: + return "2048x1536 (QXGA)"; + case FRAMESIZE_QHD: + return "2560x1440 (QHD)"; + case FRAMESIZE_WQXGA: + return "2560x1600 (WQXGA)"; + case FRAMESIZE_P_FHD: + return "1080x1920 (P_FHD)"; + case FRAMESIZE_QSXGA: + return "2560x1920 (QSXGA)"; + default: + return "UNKNOWN"; + } +} + +static const char *pixel_format_to_str(pixformat_t format) { + switch (format) { + case PIXFORMAT_RGB565: + return "RGB565"; + case PIXFORMAT_YUV422: + return "YUV422"; + case PIXFORMAT_YUV420: + return "YUV420"; + case PIXFORMAT_GRAYSCALE: + return "GRAYSCALE"; + case PIXFORMAT_JPEG: + return "JPEG"; + case PIXFORMAT_RGB888: + return "RGB888"; + case PIXFORMAT_RAW: + return "RAW"; + case PIXFORMAT_RGB444: + return "RGB444"; + case PIXFORMAT_RGB555: + return "RGB555"; + default: + return "UNKNOWN"; + } +} + /* ---------------- public API (derivated) ---------------- */ void ESP32Camera::setup() { #ifdef USE_I2C @@ -68,64 +136,9 @@ void ESP32Camera::dump_config() { this->name_.c_str(), YESNO(this->is_internal()), conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3, conf.pin_d4, conf.pin_d5, conf.pin_d6, conf.pin_d7, conf.pin_vsync, conf.pin_href, conf.pin_pclk, conf.pin_xclk, conf.xclk_freq_hz, conf.pin_sccb_sda, conf.pin_sccb_scl, conf.pin_reset); - switch (this->config_.frame_size) { - case FRAMESIZE_QQVGA: - ESP_LOGCONFIG(TAG, " Resolution: 160x120 (QQVGA)"); - break; - case FRAMESIZE_QCIF: - ESP_LOGCONFIG(TAG, " Resolution: 176x155 (QCIF)"); - break; - case FRAMESIZE_HQVGA: - ESP_LOGCONFIG(TAG, " Resolution: 240x176 (HQVGA)"); - break; - case FRAMESIZE_QVGA: - ESP_LOGCONFIG(TAG, " Resolution: 320x240 (QVGA)"); - break; - case FRAMESIZE_CIF: - ESP_LOGCONFIG(TAG, " Resolution: 400x296 (CIF)"); - break; - case FRAMESIZE_VGA: - ESP_LOGCONFIG(TAG, " Resolution: 640x480 (VGA)"); - break; - case FRAMESIZE_SVGA: - ESP_LOGCONFIG(TAG, " Resolution: 800x600 (SVGA)"); - break; - case FRAMESIZE_XGA: - ESP_LOGCONFIG(TAG, " Resolution: 1024x768 (XGA)"); - break; - case FRAMESIZE_SXGA: - ESP_LOGCONFIG(TAG, " Resolution: 1280x1024 (SXGA)"); - break; - case FRAMESIZE_UXGA: - ESP_LOGCONFIG(TAG, " Resolution: 1600x1200 (UXGA)"); - break; - case FRAMESIZE_FHD: - ESP_LOGCONFIG(TAG, " Resolution: 1920x1080 (FHD)"); - break; - case FRAMESIZE_P_HD: - ESP_LOGCONFIG(TAG, " Resolution: 720x1280 (P_HD)"); - break; - case FRAMESIZE_P_3MP: - ESP_LOGCONFIG(TAG, " Resolution: 864x1536 (P_3MP)"); - break; - case FRAMESIZE_QXGA: - ESP_LOGCONFIG(TAG, " Resolution: 2048x1536 (QXGA)"); - break; - case FRAMESIZE_QHD: - ESP_LOGCONFIG(TAG, " Resolution: 2560x1440 (QHD)"); - break; - case FRAMESIZE_WQXGA: - ESP_LOGCONFIG(TAG, " Resolution: 2560x1600 (WQXGA)"); - break; - case FRAMESIZE_P_FHD: - ESP_LOGCONFIG(TAG, " Resolution: 1080x1920 (P_FHD)"); - break; - case FRAMESIZE_QSXGA: - ESP_LOGCONFIG(TAG, " Resolution: 2560x1920 (QSXGA)"); - break; - default: - break; - } + + ESP_LOGCONFIG(TAG, " Resolution: %s", frame_size_to_str(this->config_.frame_size)); + ESP_LOGCONFIG(TAG, " Pixel Format: %s", pixel_format_to_str(this->config_.pixel_format)); if (this->is_failed()) { ESP_LOGE(TAG, " Setup Failed: %s", esp_err_to_name(this->init_error_)); @@ -184,8 +197,19 @@ void ESP32Camera::loop() { // check if we can return the image if (this->can_return_image_()) { // return image - auto *fb = this->current_image_->get_raw_buffer(); - xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); +#ifdef USE_ESP32_CAMERA_JPEG_CONVERSION + if (this->config_.pixel_format != PIXFORMAT_JPEG && this->config_.jpeg_quality > 0) { + // for non-JPEG format, we need to free the data and raw buffer + auto *jpg_buf = this->current_image_->get_data_buffer(); + free(jpg_buf); // NOLINT(cppcoreguidelines-no-malloc) + auto *fb = this->current_image_->get_raw_buffer(); + this->fb_allocator_.deallocate(fb, 1); + } else +#endif + { + auto *fb = this->current_image_->get_raw_buffer(); + xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); + } this->current_image_.reset(); } @@ -212,6 +236,38 @@ void ESP32Camera::loop() { xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); return; } + +#ifdef USE_ESP32_CAMERA_JPEG_CONVERSION + if (this->config_.pixel_format != PIXFORMAT_JPEG && this->config_.jpeg_quality > 0) { + // for non-JPEG format, we need to convert the frame to JPEG + uint8_t *jpg_buf; + size_t jpg_buf_len; + size_t width = fb->width; + size_t height = fb->height; + struct timeval timestamp = fb->timestamp; + bool ok = frame2jpg(fb, 100 - this->config_.jpeg_quality, &jpg_buf, &jpg_buf_len); + // return the original frame buffer to the queue + xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); + if (!ok) { + ESP_LOGE(TAG, "Failed to convert frame to JPEG!"); + return; + } + // create a new camera_fb_t for the JPEG data + fb = this->fb_allocator_.allocate(1); + if (fb == nullptr) { + ESP_LOGE(TAG, "Failed to allocate memory for camera frame buffer!"); + free(jpg_buf); // NOLINT(cppcoreguidelines-no-malloc) + return; + } + memset(fb, 0, sizeof(camera_fb_t)); + fb->buf = jpg_buf; + fb->len = jpg_buf_len; + fb->width = width; + fb->height = height; + fb->format = PIXFORMAT_JPEG; + fb->timestamp = timestamp; + } +#endif this->current_image_ = std::make_shared(fb, this->single_requesters_ | this->stream_requesters_); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE @@ -342,6 +398,37 @@ void ESP32Camera::set_frame_size(ESP32CameraFrameSize size) { break; } } +void ESP32Camera::set_pixel_format(ESP32CameraPixelFormat format) { + switch (format) { + case ESP32_PIXEL_FORMAT_RGB565: + this->config_.pixel_format = PIXFORMAT_RGB565; + break; + case ESP32_PIXEL_FORMAT_YUV422: + this->config_.pixel_format = PIXFORMAT_YUV422; + break; + case ESP32_PIXEL_FORMAT_YUV420: + this->config_.pixel_format = PIXFORMAT_YUV420; + break; + case ESP32_PIXEL_FORMAT_GRAYSCALE: + this->config_.pixel_format = PIXFORMAT_GRAYSCALE; + break; + case ESP32_PIXEL_FORMAT_JPEG: + this->config_.pixel_format = PIXFORMAT_JPEG; + break; + case ESP32_PIXEL_FORMAT_RGB888: + this->config_.pixel_format = PIXFORMAT_RGB888; + break; + case ESP32_PIXEL_FORMAT_RAW: + this->config_.pixel_format = PIXFORMAT_RAW; + break; + case ESP32_PIXEL_FORMAT_RGB444: + this->config_.pixel_format = PIXFORMAT_RGB444; + break; + case ESP32_PIXEL_FORMAT_RGB555: + this->config_.pixel_format = PIXFORMAT_RGB555; + break; + } +} void ESP32Camera::set_jpeg_quality(uint8_t quality) { this->config_.jpeg_quality = quality; } void ESP32Camera::set_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; } void ESP32Camera::set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; } diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index eea93b7e01..9fbd3848f2 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -41,6 +41,18 @@ enum ESP32CameraFrameSize { ESP32_CAMERA_SIZE_2560X1920, // QSXGA }; +enum ESP32CameraPixelFormat { + ESP32_PIXEL_FORMAT_RGB565, + ESP32_PIXEL_FORMAT_YUV422, + ESP32_PIXEL_FORMAT_YUV420, + ESP32_PIXEL_FORMAT_GRAYSCALE, + ESP32_PIXEL_FORMAT_JPEG, + ESP32_PIXEL_FORMAT_RGB888, + ESP32_PIXEL_FORMAT_RAW, + ESP32_PIXEL_FORMAT_RGB444, + ESP32_PIXEL_FORMAT_RGB555, +}; + enum ESP32AgcGainCeiling { ESP32_GAINCEILING_2X = GAINCEILING_2X, ESP32_GAINCEILING_4X = GAINCEILING_4X, @@ -126,6 +138,7 @@ class ESP32Camera : public camera::Camera { void set_reset_pin(uint8_t pin); void set_power_down_pin(uint8_t pin); /* -- image */ + void set_pixel_format(ESP32CameraPixelFormat format); void set_frame_size(ESP32CameraFrameSize size); void set_jpeg_quality(uint8_t quality); void set_vertical_flip(bool vertical_flip); @@ -220,6 +233,7 @@ class ESP32Camera : public camera::Camera { #ifdef USE_I2C i2c::InternalI2CBus *i2c_bus_{nullptr}; #endif // USE_I2C + RAMAllocator fb_allocator_{RAMAllocator::ALLOC_INTERNAL}; }; class ESP32CameraImageTrigger : public Trigger, public camera::CameraListener { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5559ec88d0..a7fb9f197c 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -43,6 +43,7 @@ #define USE_DEVICES #define USE_DISPLAY #define USE_ENTITY_ICON +#define USE_ESP32_CAMERA_JPEG_CONVERSION #define USE_ESP32_HOSTED #define USE_ESP32_IMPROV_STATE_CALLBACK #define USE_EVENT diff --git a/tests/test_build_components/common/i2c_camera/esp32-idf.yaml b/tests/test_build_components/common/i2c_camera/esp32-idf.yaml index 443ebbebd9..07ab6cdc8d 100644 --- a/tests/test_build_components/common/i2c_camera/esp32-idf.yaml +++ b/tests/test_build_components/common/i2c_camera/esp32-idf.yaml @@ -30,6 +30,7 @@ esp32_camera: resolution: 640x480 jpeg_quality: 10 frame_buffer_location: PSRAM + pixel_format: JPEG on_image: then: - lambda: |- From 4cc1e6a9103dc804e9e4516ff1f6fe313701ccf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Thu, 19 Feb 2026 15:23:22 +0100 Subject: [PATCH 38/43] [esp32_ble_server] add test for lambda characteristic (#14091) --- tests/components/esp32_ble_server/common.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/components/esp32_ble_server/common.yaml b/tests/components/esp32_ble_server/common.yaml index e9576a8262..7fe0b2eb5f 100644 --- a/tests/components/esp32_ble_server/common.yaml +++ b/tests/components/esp32_ble_server/common.yaml @@ -33,6 +33,10 @@ esp32_ble_server: - uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc42d advertise: false characteristics: + - id: test_lambda_characteristic + uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc12c + read: true + value: !lambda return { 1, 2 }; - id: test_change_characteristic uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc11c read: true From 7b53a9895000219aac5895de48c5bd15715d7aea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 08:39:44 -0600 Subject: [PATCH 39/43] [socket] Log error when UDP socket requested on LWIP TCP-only platforms (#14089) --- esphome/components/socket/lwip_raw_tcp_impl.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index aa37386d70..6e95f5bc7a 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -680,6 +680,11 @@ class LWIPRawListenImpl final : public LWIPRawImpl { }; std::unique_ptr socket(int domain, int type, int protocol) { + if (type != SOCK_STREAM) { + ESP_LOGE(TAG, "UDP sockets not supported on this platform, use WiFiUDP"); + errno = EPROTOTYPE; + return nullptr; + } auto *pcb = tcp_new(); if (pcb == nullptr) return nullptr; From 6daca09794d7d48478e4c6751907ae5a4b5aa7b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 08:40:08 -0600 Subject: [PATCH 40/43] [logger] Replace LogListener virtual interface with LogCallback struct (#14084) --- esphome/components/api/api_server.cpp | 5 +- esphome/components/api/api_server.h | 6 +- esphome/components/ble_nus/ble_nus.cpp | 5 +- esphome/components/ble_nus/ble_nus.h | 9 +-- esphome/components/logger/logger.h | 66 ++++++++++---------- esphome/components/mqtt/mqtt_client.cpp | 5 +- esphome/components/mqtt/mqtt_client.h | 9 +-- esphome/components/syslog/esphome_syslog.cpp | 7 ++- esphome/components/syslog/esphome_syslog.h | 5 +- esphome/components/web_server/web_server.cpp | 5 +- esphome/components/web_server/web_server.h | 11 +--- 11 files changed, 65 insertions(+), 68 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 67a117e68f..1a1d0b229b 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -92,7 +92,10 @@ void APIServer::setup() { #ifdef USE_LOGGER if (logger::global_logger != nullptr) { - logger::global_logger->add_log_listener(this); + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); } #endif diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 323acc2efb..3b9ba0e23b 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -37,10 +37,6 @@ struct SavedNoisePsk { class APIServer : public Component, public Controller -#ifdef USE_LOGGER - , - public logger::LogListener -#endif #ifdef USE_CAMERA , public camera::CameraListener @@ -56,7 +52,7 @@ class APIServer : public Component, void on_shutdown() override; bool teardown() override; #ifdef USE_LOGGER - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); #endif #ifdef USE_CAMERA void on_camera_image(const std::shared_ptr &image) override; diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index 0de65b623f..a10132eb3e 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -87,7 +87,10 @@ void BLENUS::setup() { global_ble_nus = this; #ifdef USE_LOGGER if (logger::global_logger != nullptr && this->expose_log_) { - logger::global_logger->add_log_listener(this); + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); } #endif } diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index ef20fc5e5b..b2b0ee7713 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -10,12 +10,7 @@ namespace esphome::ble_nus { -class BLENUS : public Component -#ifdef USE_LOGGER - , - public logger::LogListener -#endif -{ +class BLENUS : public Component { enum TxStatus { TX_DISABLED, TX_ENABLED, @@ -29,7 +24,7 @@ class BLENUS : public Component size_t write_array(const uint8_t *data, size_t len); void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } #ifdef USE_LOGGER - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); #endif protected: diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 835542dd8f..2a7552af92 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -40,26 +40,25 @@ struct device; namespace esphome::logger { -/** Interface for receiving log messages without std::function overhead. +/** Lightweight callback for receiving log messages without virtual dispatch overhead. * - * Components can implement this interface instead of using lambdas with std::function - * to reduce flash usage from std::function type erasure machinery. + * Replaces the former LogListener virtual interface to eliminate per-implementer + * vtable sub-tables and thunk code (~39 bytes saved per class that used LogListener). * * Usage: - * class MyComponent : public Component, public LogListener { - * public: - * void setup() override { - * if (logger::global_logger != nullptr) - * logger::global_logger->add_log_listener(this); - * } - * void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override { - * // Handle log message - * } - * }; + * // In your component's setup(): + * if (logger::global_logger != nullptr) + * logger::global_logger->add_log_callback( + * this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + * static_cast(self)->on_log(level, tag, message, message_len); + * }); */ -class LogListener { - public: - virtual void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) = 0; +struct LogCallback { + void *instance; + void (*fn)(void *, uint8_t, const char *, const char *, size_t); + void invoke(uint8_t level, const char *tag, const char *message, size_t message_len) const { + this->fn(this->instance, level, tag, message, message_len); + } }; #ifdef USE_LOGGER_LEVEL_LISTENERS @@ -187,11 +186,13 @@ class Logger : public Component { inline uint8_t level_for(const char *tag); #ifdef USE_LOG_LISTENERS - /// Register a log listener to receive log messages - void add_log_listener(LogListener *listener) { this->log_listeners_.push_back(listener); } + /// Register a log callback to receive log messages + void add_log_callback(void *instance, void (*fn)(void *, uint8_t, const char *, const char *, size_t)) { + this->log_callbacks_.push_back(LogCallback{instance, fn}); + } #else /// No-op when log listeners are disabled - void add_log_listener(LogListener *listener) {} + void add_log_callback(void *instance, void (*fn)(void *, uint8_t, const char *, const char *, size_t)) {} #endif #ifdef USE_LOGGER_LEVEL_LISTENERS @@ -253,11 +254,11 @@ class Logger : public Component { } #endif - // Helper to notify log listeners + // Helper to notify log callbacks inline void HOT notify_listeners_(uint8_t level, const char *tag, const LogBuffer &buf) { #ifdef USE_LOG_LISTENERS - for (auto *listener : this->log_listeners_) - listener->on_log(level, tag, buf.data, buf.pos); + for (auto &cb : this->log_callbacks_) + cb.invoke(level, tag, buf.data, buf.pos); #endif } @@ -341,8 +342,8 @@ class Logger : public Component { std::map log_levels_{}; #endif #ifdef USE_LOG_LISTENERS - StaticVector - log_listeners_; // Log message listeners (API, MQTT, syslog, etc.) + StaticVector + log_callbacks_; // Log message callbacks (API, MQTT, syslog, etc.) #endif #ifdef USE_LOGGER_LEVEL_LISTENERS std::vector level_listeners_; // Log level change listeners @@ -478,15 +479,16 @@ class Logger : public Component { }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class LoggerMessageTrigger : public Trigger, public LogListener { +class LoggerMessageTrigger : public Trigger { public: - explicit LoggerMessageTrigger(Logger *parent, uint8_t level) : level_(level) { parent->add_log_listener(this); } - - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override { - (void) message_len; - if (level <= this->level_) { - this->trigger(level, tag, message); - } + explicit LoggerMessageTrigger(Logger *parent, uint8_t level) : level_(level) { + parent->add_log_callback(this, + [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + auto *trigger = static_cast(self); + if (level <= trigger->level_) { + trigger->trigger(level, tag, message); + } + }); } protected: diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 90b423c386..9905b4677e 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -64,7 +64,10 @@ void MQTTClientComponent::setup() { }); #ifdef USE_LOGGER if (this->is_log_message_enabled() && logger::global_logger != nullptr) { - logger::global_logger->add_log_listener(this); + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); } #endif diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 38bc0b4da3..21edd53eda 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -99,12 +99,7 @@ enum MQTTClientState { class MQTTComponent; -class MQTTClientComponent : public Component -#ifdef USE_LOGGER - , - public logger::LogListener -#endif -{ +class MQTTClientComponent : public Component { public: MQTTClientComponent(); @@ -252,7 +247,7 @@ class MQTTClientComponent : public Component float get_setup_priority() const override; #ifdef USE_LOGGER - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); #endif void on_message(const std::string &topic, const std::string &payload); diff --git a/esphome/components/syslog/esphome_syslog.cpp b/esphome/components/syslog/esphome_syslog.cpp index 376de54db4..790d08ffa6 100644 --- a/esphome/components/syslog/esphome_syslog.cpp +++ b/esphome/components/syslog/esphome_syslog.cpp @@ -18,7 +18,12 @@ constexpr int LOG_LEVEL_TO_SYSLOG_SEVERITY[] = { 7 // VERY_VERBOSE }; -void Syslog::setup() { logger::global_logger->add_log_listener(this); } +void Syslog::setup() { + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); +} void Syslog::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { this->log_(level, tag, message, message_len); diff --git a/esphome/components/syslog/esphome_syslog.h b/esphome/components/syslog/esphome_syslog.h index bde6ab5ed4..be4fa91436 100644 --- a/esphome/components/syslog/esphome_syslog.h +++ b/esphome/components/syslog/esphome_syslog.h @@ -2,17 +2,16 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/components/logger/logger.h" #include "esphome/components/udp/udp_component.h" #include "esphome/components/time/real_time_clock.h" #ifdef USE_NETWORK namespace esphome::syslog { -class Syslog : public Component, public Parented, public logger::LogListener { +class Syslog : public Component, public Parented { public: Syslog(int level, time::RealTimeClock *time) : log_level_(level), time_(time) {} void setup() override; - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); void set_strip(bool strip) { this->strip_ = strip; } void set_facility(int facility) { this->facility_ = facility; } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index e3d131f58e..a44a47379e 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -395,7 +395,10 @@ void WebServer::setup() { #ifdef USE_LOGGER if (logger::global_logger != nullptr && this->expose_log_) { - logger::global_logger->add_log_listener(this); + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); } #endif diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 026da763ea..6afe618b59 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -186,14 +186,7 @@ class DeferredUpdateEventSourceList : public std::list Date: Thu, 19 Feb 2026 08:40:23 -0600 Subject: [PATCH 41/43] [core] Devirtualize call_loop() and mark_failed() in Component (#14083) --- esphome/core/component.cpp | 12 +++++------- esphome/core/component.h | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index ba0f1663b9..f283a69064 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -207,7 +207,7 @@ bool Component::cancel_retry(uint32_t id) { #pragma GCC diagnostic pop } -void Component::call_loop() { this->loop(); } +void Component::call_loop_() { this->loop(); } void Component::call_setup() { this->setup(); } void Component::call_dump_config() { this->dump_config(); @@ -258,11 +258,11 @@ void Component::call() { case COMPONENT_STATE_SETUP: // State setup: Call first loop and set state to loop this->set_component_state_(COMPONENT_STATE_LOOP); - this->call_loop(); + this->call_loop_(); break; case COMPONENT_STATE_LOOP: // State loop: Call loop - this->call_loop(); + this->call_loop_(); break; case COMPONENT_STATE_FAILED: // State failed: Do nothing @@ -497,16 +497,14 @@ void Component::set_setup_priority(float priority) { bool Component::has_overridden_loop() const { #if defined(USE_HOST) || defined(CLANG_TIDY) - bool loop_overridden = true; - bool call_loop_overridden = true; + return true; #else #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpmf-conversions" bool loop_overridden = (void *) (this->*(&Component::loop)) != (void *) (&Component::loop); - bool call_loop_overridden = (void *) (this->*(&Component::call_loop)) != (void *) (&Component::call_loop); #pragma GCC diagnostic pop + return loop_overridden; #endif - return loop_overridden || call_loop_overridden; } PollingComponent::PollingComponent(uint32_t update_interval) : update_interval_(update_interval) {} diff --git a/esphome/core/component.h b/esphome/core/component.h index 6f7f77dbc1..d4dad3c9a6 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -165,7 +165,7 @@ class Component { * For example, i2c based components can check if the remote device is responding and otherwise * mark the component as failed. Eventually this will also enable smart status LEDs. */ - virtual void mark_failed(); + void mark_failed(); // Remove before 2026.6.0 ESPDEPRECATED("Use mark_failed(LOG_STR(\"static string literal\")) instead. Do NOT use .c_str() from temporary " @@ -286,7 +286,7 @@ class Component { protected: friend class Application; - virtual void call_loop(); + void call_loop_(); virtual void call_setup(); virtual void call_dump_config(); From 535980b9bd37ebf405a6305e34e980e3f16a5af6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 08:40:41 -0600 Subject: [PATCH 42/43] [cse7761] Use constexpr for compile-time constants (#14081) --- esphome/components/cse7761/cse7761.cpp | 38 +++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/esphome/components/cse7761/cse7761.cpp b/esphome/components/cse7761/cse7761.cpp index 7c5ee833a4..f4966357d4 100644 --- a/esphome/components/cse7761/cse7761.cpp +++ b/esphome/components/cse7761/cse7761.cpp @@ -15,29 +15,29 @@ static const char *const TAG = "cse7761"; * https://github.com/arendst/Tasmota/blob/development/tasmota/xnrg_19_cse7761.ino \*********************************************************************************************/ -static const int CSE7761_UREF = 42563; // RmsUc -static const int CSE7761_IREF = 52241; // RmsIAC -static const int CSE7761_PREF = 44513; // PowerPAC +static constexpr int CSE7761_UREF = 42563; // RmsUc +static constexpr int CSE7761_IREF = 52241; // RmsIAC +static constexpr int CSE7761_PREF = 44513; // PowerPAC -static const uint8_t CSE7761_REG_SYSCON = 0x00; // (2) System Control Register (0x0A04) -static const uint8_t CSE7761_REG_EMUCON = 0x01; // (2) Metering control register (0x0000) -static const uint8_t CSE7761_REG_EMUCON2 = 0x13; // (2) Metering control register 2 (0x0001) -static const uint8_t CSE7761_REG_PULSE1SEL = 0x1D; // (2) Pin function output select register (0x3210) +static constexpr uint8_t CSE7761_REG_SYSCON = 0x00; // (2) System Control Register (0x0A04) +static constexpr uint8_t CSE7761_REG_EMUCON = 0x01; // (2) Metering control register (0x0000) +static constexpr uint8_t CSE7761_REG_EMUCON2 = 0x13; // (2) Metering control register 2 (0x0001) +static constexpr uint8_t CSE7761_REG_PULSE1SEL = 0x1D; // (2) Pin function output select register (0x3210) -static const uint8_t CSE7761_REG_RMSIA = 0x24; // (3) The effective value of channel A current (0x000000) -static const uint8_t CSE7761_REG_RMSIB = 0x25; // (3) The effective value of channel B current (0x000000) -static const uint8_t CSE7761_REG_RMSU = 0x26; // (3) Voltage RMS (0x000000) -static const uint8_t CSE7761_REG_POWERPA = 0x2C; // (4) Channel A active power, update rate 27.2Hz (0x00000000) -static const uint8_t CSE7761_REG_POWERPB = 0x2D; // (4) Channel B active power, update rate 27.2Hz (0x00000000) -static const uint8_t CSE7761_REG_SYSSTATUS = 0x43; // (1) System status register +static constexpr uint8_t CSE7761_REG_RMSIA = 0x24; // (3) The effective value of channel A current (0x000000) +static constexpr uint8_t CSE7761_REG_RMSIB = 0x25; // (3) The effective value of channel B current (0x000000) +static constexpr uint8_t CSE7761_REG_RMSU = 0x26; // (3) Voltage RMS (0x000000) +static constexpr uint8_t CSE7761_REG_POWERPA = 0x2C; // (4) Channel A active power, update rate 27.2Hz (0x00000000) +static constexpr uint8_t CSE7761_REG_POWERPB = 0x2D; // (4) Channel B active power, update rate 27.2Hz (0x00000000) +static constexpr uint8_t CSE7761_REG_SYSSTATUS = 0x43; // (1) System status register -static const uint8_t CSE7761_REG_COEFFCHKSUM = 0x6F; // (2) Coefficient checksum -static const uint8_t CSE7761_REG_RMSIAC = 0x70; // (2) Channel A effective current conversion coefficient +static constexpr uint8_t CSE7761_REG_COEFFCHKSUM = 0x6F; // (2) Coefficient checksum +static constexpr uint8_t CSE7761_REG_RMSIAC = 0x70; // (2) Channel A effective current conversion coefficient -static const uint8_t CSE7761_SPECIAL_COMMAND = 0xEA; // Start special command -static const uint8_t CSE7761_CMD_RESET = 0x96; // Reset command, after receiving the command, the chip resets -static const uint8_t CSE7761_CMD_CLOSE_WRITE = 0xDC; // Close write operation -static const uint8_t CSE7761_CMD_ENABLE_WRITE = 0xE5; // Enable write operation +static constexpr uint8_t CSE7761_SPECIAL_COMMAND = 0xEA; // Start special command +static constexpr uint8_t CSE7761_CMD_RESET = 0x96; // Reset command, after receiving the command, the chip resets +static constexpr uint8_t CSE7761_CMD_CLOSE_WRITE = 0xDC; // Close write operation +static constexpr uint8_t CSE7761_CMD_ENABLE_WRITE = 0xE5; // Enable write operation enum CSE7761 { RMS_IAC, RMS_IBC, RMS_UC, POWER_PAC, POWER_PBC, POWER_SC, ENERGY_AC, ENERGY_BC }; From 01a46f665f550085022bed7d994e043045f5553a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:42:22 -0500 Subject: [PATCH 43/43] Bump esptool from 5.1.0 to 5.2.0 (#14058) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2e6268284a..be3445dceb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ tzlocal==5.3.1 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.19 -esptool==5.1.0 +esptool==5.2.0 click==8.1.7 esphome-dashboard==20260210.0 aioesphomeapi==44.0.0